Initial commit
This commit is contained in:
57
.gitignore
vendored
Normal file
57
.gitignore
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# Built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Version control
|
||||
.svn/
|
||||
|
||||
# OS-specific files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Project Specific
|
||||
bugs-encontrados.txt
|
||||
opencode.txt
|
||||
DocmostApp.md
|
||||
|
||||
# App build
|
||||
app/build/
|
||||
32
README.md
Normal file
32
README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Docmost App
|
||||
|
||||
Cliente Android no oficial para [Docmost](https://docmost.com) — una wiki colaborativa de código abierto.
|
||||
|
||||
## Funcionalidades
|
||||
|
||||
- **Autenticación por cookie** — login contra servidor Docmost Community Edition
|
||||
- **Lista de espacios** — espacios expandibles con páginas en sidebar
|
||||
- **Editor Markdown** — edición de páginas con TipTap → Markdown, toggle Edit/Preview con Markwon
|
||||
- **Iconos de espacio** — carga y muestra desde el servidor (OkHttp + cache Lru)
|
||||
- **Editor de espacio** — modificar nombre, descripción, icono (subir/eliminar)
|
||||
- **Emojis de página** — muestra el `icon` emoji de cada página
|
||||
- **UI Material 3** — MaterialCardView, Dynamic Colors, tema en español
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Android 8.0 (API 26) o superior
|
||||
- Servidor Docmost (Community Edition) accesible desde el dispositivo
|
||||
|
||||
## Compilación
|
||||
|
||||
```bash
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
## Capturas de pantalla
|
||||
|
||||
*(pendiente)*
|
||||
|
||||
## Licencia
|
||||
|
||||
MIT
|
||||
80
app/build.gradle.kts
Normal file
80
app/build.gradle.kts
Normal file
@@ -0,0 +1,80 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
kotlin("kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.docmost.app"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.docmost.app"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
||||
implementation("androidx.activity:activity-ktx:1.8.2")
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
|
||||
// Biometric authentication
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
|
||||
// Encrypted SharedPreferences
|
||||
implementation("androidx.security:security-crypto:1.0.0")
|
||||
|
||||
// SwipeRefreshLayout
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
|
||||
// Markwon - Markdown rendering
|
||||
implementation("io.noties.markwon:core:4.6.2")
|
||||
implementation("io.noties.markwon:ext-strikethrough:4.6.2")
|
||||
implementation("io.noties.markwon:ext-tables:4.6.2")
|
||||
implementation("io.noties.markwon:ext-tasklist:4.6.2")
|
||||
|
||||
// Coil - Image loading
|
||||
implementation("io.coil-kt:coil:2.7.0")
|
||||
|
||||
// Room - Local database for sync queue
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
kapt("androidx.room:room-compiler:2.6.1")
|
||||
|
||||
// WorkManager - Background sync
|
||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||
}
|
||||
6
app/proguard-rules.pro
vendored
Normal file
6
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
-keep class com.docmost.app.api.** { *; }
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn okio.**
|
||||
47
app/src/main/AndroidManifest.xml
Normal file
47
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
|
||||
<application
|
||||
android:name=".DocmostApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_docmost"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Docmost"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<activity
|
||||
android:name=".ui.LoginActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.SpacesActivity"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.PageEditorActivity"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="uiMode|orientation|screenSize" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.VersionViewerActivity"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="uiMode|orientation|screenSize" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
2
app/src/main/assets/editor/checklist.umd.js
Normal file
2
app/src/main/assets/editor/checklist.umd.js
Normal file
@@ -0,0 +1,2 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode('.cdx-checklist{gap:6px;display:flex;flex-direction:column}.cdx-checklist__item{display:flex;box-sizing:content-box;align-items:flex-start}.cdx-checklist__item-text{outline:none;flex-grow:1;line-height:1.57em}.cdx-checklist__item-checkbox{width:22px;height:22px;display:flex;align-items:center;margin-right:8px;margin-top:calc(.785em - 11px);cursor:pointer}.cdx-checklist__item-checkbox svg{opacity:0;height:20px;width:20px;position:absolute;left:-1px;top:-1px;max-height:20px}@media (hover: hover){.cdx-checklist__item-checkbox:not(.cdx-checklist__item-checkbox--no-hover):hover .cdx-checklist__item-checkbox-check svg{opacity:1}}.cdx-checklist__item-checkbox-check{cursor:pointer;display:inline-block;flex-shrink:0;position:relative;width:20px;height:20px;box-sizing:border-box;margin-left:0;border-radius:5px;border:1px solid #C9C9C9;background:#fff}.cdx-checklist__item-checkbox-check:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;border-radius:100%;background-color:#369fff;visibility:hidden;pointer-events:none;transform:scale(1);transition:transform .4s ease-out,opacity .4s}@media (hover: hover){.cdx-checklist__item--checked .cdx-checklist__item-checkbox:not(.cdx-checklist__item--checked .cdx-checklist__item-checkbox--no-hover):hover .cdx-checklist__item-checkbox-check{background:#0059AB;border-color:#0059ab}}.cdx-checklist__item--checked .cdx-checklist__item-checkbox-check{background:#369FFF;border-color:#369fff}.cdx-checklist__item--checked .cdx-checklist__item-checkbox-check svg{opacity:1}.cdx-checklist__item--checked .cdx-checklist__item-checkbox-check svg path{stroke:#fff}.cdx-checklist__item--checked .cdx-checklist__item-checkbox-check:before{opacity:0;visibility:visible;transform:scale(2.5)}')),document.head.appendChild(e)}}catch(c){console.error("vite-plugin-css-injected-by-js",c)}})();
|
||||
(function(l,c){typeof exports=="object"&&typeof module<"u"?module.exports=c():typeof define=="function"&&define.amd?define(c):(l=typeof globalThis<"u"?globalThis:l||self,l.Checklist=c())})(this,function(){"use strict";const l="",c='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M7 12L10.4884 15.8372C10.5677 15.9245 10.705 15.9245 10.7844 15.8372L17 9"/></svg>',g='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M9.2 12L11.0586 13.8586C11.1367 13.9367 11.2633 13.9367 11.3414 13.8586L14.7 10.5"/><rect width="14" height="14" x="5" y="5" stroke="currentColor" stroke-width="2" rx="4"/></svg>';function d(){const s=document.activeElement,t=window.getSelection().getRangeAt(0),n=t.cloneRange();return n.selectNodeContents(s),n.setStart(t.endContainer,t.endOffset),n.extractContents()}function f(s){const e=document.createElement("div");return e.appendChild(s),e.innerHTML}function o(s,e=null,t={}){const n=document.createElement(s);Array.isArray(e)?n.classList.add(...e):e&&n.classList.add(e);for(const i in t)n[i]=t[i];return n}function m(s){return s.innerHTML.replace("<br>"," ").trim()}function p(s,e=!1,t=void 0){const n=document.createRange(),i=window.getSelection();n.selectNodeContents(s),t!==void 0&&(n.setStart(s,t),n.setEnd(s,t)),n.collapse(e),i.removeAllRanges(),i.addRange(n)}Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector),Element.prototype.closest||(Element.prototype.closest=function(s){let e=this;if(!document.documentElement.contains(e))return null;do{if(e.matches(s))return e;e=e.parentElement||e.parentNode}while(e!==null&&e.nodeType===1);return null});class C{static get isReadOnlySupported(){return!0}static get enableLineBreaks(){return!0}static get toolbox(){return{icon:g,title:"Checklist"}}static get conversionConfig(){return{export:e=>e.items.map(({text:t})=>t).join(". "),import:e=>({items:[{text:e,checked:!1}]})}}constructor({data:e,config:t,api:n,readOnly:i}){this._elements={wrapper:null,items:[]},this.readOnly=i,this.api=n,this.data=e||{}}render(){return this._elements.wrapper=o("div",[this.CSS.baseBlock,this.CSS.wrapper]),this.data.items||(this.data.items=[{text:"",checked:!1}]),this.data.items.forEach(e=>{const t=this.createChecklistItem(e);this._elements.wrapper.appendChild(t)}),this.readOnly?this._elements.wrapper:(this._elements.wrapper.addEventListener("keydown",e=>{const[t,n]=[13,8];switch(e.keyCode){case t:this.enterPressed(e);break;case n:this.backspace(e);break}},!1),this._elements.wrapper.addEventListener("click",e=>{this.toggleCheckbox(e)}),this._elements.wrapper)}save(){let e=this.items.map(t=>{const n=this.getItemInput(t);return{text:m(n),checked:t.classList.contains(this.CSS.itemChecked)}});return e=e.filter(t=>t.text.trim().length!==0),{items:e}}validate(e){return!!e.items.length}toggleCheckbox(e){const t=e.target.closest(`.${this.CSS.item}`),n=t.querySelector(`.${this.CSS.checkboxContainer}`);n.contains(e.target)&&(t.classList.toggle(this.CSS.itemChecked),n.classList.add(this.CSS.noHover),n.addEventListener("mouseleave",()=>this.removeSpecialHoverBehavior(n),{once:!0}))}createChecklistItem(e={}){const t=o("div",this.CSS.item),n=o("span",this.CSS.checkbox),i=o("div",this.CSS.checkboxContainer),a=o("div",this.CSS.textField,{innerHTML:e.text?e.text:"",contentEditable:!this.readOnly});return e.checked&&t.classList.add(this.CSS.itemChecked),n.innerHTML=c,i.appendChild(n),t.appendChild(i),t.appendChild(a),t}enterPressed(e){e.preventDefault();const t=this.items,n=document.activeElement.closest(`.${this.CSS.item}`);if(t.indexOf(n)===t.length-1&&m(this.getItemInput(n)).length===0){const x=this.api.blocks.getCurrentBlockIndex();n.remove(),this.api.blocks.insert(),this.api.caret.setToBlock(x+1);return}const u=d(),h=f(u),r=this.createChecklistItem({text:h,checked:!1});this._elements.wrapper.insertBefore(r,n.nextSibling),p(this.getItemInput(r),!0)}backspace(e){const t=e.target.closest(`.${this.CSS.item}`),n=this.items.indexOf(t),i=this.items[n-1];if(!i||!(window.getSelection().focusOffset===0))return;e.preventDefault();const h=d(),r=this.getItemInput(i),k=r.childNodes.length;r.appendChild(h),p(r,void 0,k),t.remove()}get CSS(){return{baseBlock:this.api.styles.block,wrapper:"cdx-checklist",item:"cdx-checklist__item",itemChecked:"cdx-checklist__item--checked",noHover:"cdx-checklist__item-checkbox--no-hover",checkbox:"cdx-checklist__item-checkbox-check",textField:"cdx-checklist__item-text",checkboxContainer:"cdx-checklist__item-checkbox"}}get items(){return Array.from(this._elements.wrapper.querySelectorAll(`.${this.CSS.item}`))}removeSpecialHoverBehavior(e){e.classList.remove(this.CSS.noHover)}getItemInput(e){return e.querySelector(`.${this.CSS.textField}`)}}return C});
|
||||
11
app/src/main/assets/editor/code.umd.js
Normal file
11
app/src/main/assets/editor/code.umd.js
Normal file
@@ -0,0 +1,11 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ce-code__textarea{min-height:200px;font-family:Menlo,Monaco,Consolas,Courier New,monospace;color:#41314e;line-height:1.6em;font-size:12px;background:#f8f7fa;border:1px solid #f1f1f4;box-shadow:none;white-space:pre;word-wrap:normal;overflow-x:auto;resize:vertical}")),document.head.appendChild(e)}}catch(o){console.error("vite-plugin-css-injected-by-js",o)}})();
|
||||
(function(o,i){typeof exports=="object"&&typeof module<"u"?module.exports=i():typeof define=="function"&&define.amd?define(i):(o=typeof globalThis<"u"?globalThis:o||self,o.CodeTool=i())})(this,function(){"use strict";const o="";function i(u,t){let s="";for(;s!==`
|
||||
`&&t>0;)t=t-1,s=u.substr(t,1);return s===`
|
||||
`&&(t+=1),t}const h='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 8L5 12L9 16"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 8L19 12L15 16"/></svg>';/**
|
||||
* CodeTool for Editor.js
|
||||
*
|
||||
* @author CodeX (team@ifmo.su)
|
||||
* @copyright CodeX 2018
|
||||
* @license MIT
|
||||
* @version 2.0.0
|
||||
*/class c{static get isReadOnlySupported(){return!0}static get enableLineBreaks(){return!0}constructor({data:t,config:e,api:s,readOnly:n}){this.api=s,this.readOnly=n,this.placeholder=this.api.i18n.t(e.placeholder||c.DEFAULT_PLACEHOLDER),this.CSS={baseClass:this.api.styles.block,input:this.api.styles.input,wrapper:"ce-code",textarea:"ce-code__textarea"},this.nodes={holder:null,textarea:null},this.data={code:t.code||""},this.nodes.holder=this.drawView()}drawView(){const t=document.createElement("div"),e=document.createElement("textarea");return t.classList.add(this.CSS.baseClass,this.CSS.wrapper),e.classList.add(this.CSS.textarea,this.CSS.input),e.textContent=this.data.code,e.placeholder=this.placeholder,this.readOnly&&(e.disabled=!0),t.appendChild(e),e.addEventListener("keydown",s=>{switch(s.code){case"Tab":this.tabHandler(s);break}}),this.nodes.textarea=e,t}render(){return this.nodes.holder}save(t){return{code:t.querySelector("textarea").value}}onPaste(t){const e=t.detail.data;this.data={code:e.textContent}}get data(){return this._data}set data(t){this._data=t,this.nodes.textarea&&(this.nodes.textarea.textContent=t.code)}static get toolbox(){return{icon:h,title:"Code"}}static get DEFAULT_PLACEHOLDER(){return"Enter a code"}static get pasteConfig(){return{tags:["pre"]}}static get sanitize(){return{code:!0}}tabHandler(t){t.stopPropagation(),t.preventDefault();const e=t.target,s=t.shiftKey,n=e.selectionStart,r=e.value,a=" ";let d;if(!s)d=n+a.length,e.value=r.substring(0,n)+a+r.substring(n);else{const l=i(r,n);if(r.substr(l,a.length)!==a)return;e.value=r.substring(0,l)+r.substring(l+a.length),d=n-a.length}e.setSelectionRange(d,d)}}return c});
|
||||
9
app/src/main/assets/editor/delimiter.umd.js
Normal file
9
app/src/main/assets/editor/delimiter.umd.js
Normal file
@@ -0,0 +1,9 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode('.ce-delimiter{line-height:1.6em;width:100%;text-align:center}.ce-delimiter:before{display:inline-block;content:"***";font-size:30px;line-height:65px;height:30px;letter-spacing:.2em}')),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
|
||||
(function(i,e){typeof exports=="object"&&typeof module<"u"?module.exports=e():typeof define=="function"&&define.amd?define(e):(i=typeof globalThis<"u"?globalThis:i||self,i.Delimiter=e())})(this,function(){"use strict";const i="",e='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><line x1="6" x2="10" y1="12" y2="12" stroke="currentColor" stroke-linecap="round" stroke-width="2"/><line x1="14" x2="18" y1="12" y2="12" stroke="currentColor" stroke-linecap="round" stroke-width="2"/></svg>';/**
|
||||
* Delimiter Block for the Editor.js.
|
||||
*
|
||||
* @author CodeX (team@ifmo.su)
|
||||
* @copyright CodeX 2018
|
||||
* @license The MIT License (MIT)
|
||||
* @version 2.0.0
|
||||
*/class r{static get isReadOnlySupported(){return!0}static get contentless(){return!0}constructor({data:t,config:o,api:n}){this.api=n,this._CSS={block:this.api.styles.block,wrapper:"ce-delimiter"},this._data={},this._element=this.drawView(),this.data=t}drawView(){let t=document.createElement("DIV");return t.classList.add(this._CSS.wrapper,this._CSS.block),t}render(){return this._element}save(t){return{}}static get toolbox(){return{icon:e,title:"Delimiter"}}}return r});
|
||||
83
app/src/main/assets/editor/editorjs.umd.js
Normal file
83
app/src/main/assets/editor/editorjs.umd.js
Normal file
File diff suppressed because one or more lines are too long
2
app/src/main/assets/editor/embed.umd.js
Normal file
2
app/src/main/assets/editor/embed.umd.js
Normal file
File diff suppressed because one or more lines are too long
9
app/src/main/assets/editor/header.umd.js
Normal file
9
app/src/main/assets/editor/header.umd.js
Normal file
@@ -0,0 +1,9 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ce-header{padding:.6em 0 3px;margin:0;line-height:1.25em;outline:none}.ce-header p,.ce-header div{padding:0!important;margin:0!important}.ce-header[contentEditable=true][data-placeholder]:before{position:absolute;content:attr(data-placeholder);color:#707684;font-weight:400;display:none;cursor:text}.ce-header[contentEditable=true][data-placeholder]:empty:before{display:block}.ce-header[contentEditable=true][data-placeholder]:empty:focus:before{display:none}")),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}})();
|
||||
(function(n,r){typeof exports=="object"&&typeof module<"u"?module.exports=r():typeof define=="function"&&define.amd?define(r):(n=typeof globalThis<"u"?globalThis:n||self,n.Header=r())})(this,function(){"use strict";const n="",r='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M19 17V10.2135C19 10.1287 18.9011 10.0824 18.836 10.1367L16 12.5"/></svg>',o='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 11C16 10 19 9.5 19 12C19 13.9771 16.0684 13.9997 16.0012 16.8981C15.9999 16.9533 16.0448 17 16.1 17L19.3 17"/></svg>',a='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 11C16 10.5 16.8323 10 17.6 10C18.3677 10 19.5 10.311 19.5 11.5C19.5 12.5315 18.7474 12.9022 18.548 12.9823C18.5378 12.9864 18.5395 13.0047 18.5503 13.0063C18.8115 13.0456 20 13.3065 20 14.8C20 16 19.5 17 17.8 17C17.8 17 16 17 16 16.3"/></svg>',h='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M18 10L15.2834 14.8511C15.246 14.9178 15.294 15 15.3704 15C16.8489 15 18.7561 15 20.2 15M19 17C19 15.7187 19 14.8813 19 13.6"/></svg>',d='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 15.9C16 15.9 16.3768 17 17.8 17C19.5 17 20 15.6199 20 14.7C20 12.7323 17.6745 12.0486 16.1635 12.9894C16.094 13.0327 16 12.9846 16 12.9027V10.1C16 10.0448 16.0448 10 16.1 10H19.8"/></svg>',u='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M19.5 10C16.5 10.5 16 13.3285 16 15M16 15V15C16 16.1046 16.8954 17 18 17H18.3246C19.3251 17 20.3191 16.3492 20.2522 15.3509C20.0612 12.4958 16 12.6611 16 15Z"/></svg>',g='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M9 7L9 12M9 17V12M9 12L15 12M15 7V12M15 17L15 12"/></svg>';/**
|
||||
* Header block for the Editor.js.
|
||||
*
|
||||
* @author CodeX (team@ifmo.su)
|
||||
* @copyright CodeX 2018
|
||||
* @license MIT
|
||||
* @version 2.0.0
|
||||
*/class c{constructor({data:e,config:t,api:s,readOnly:i}){this.api=s,this.readOnly=i,this._CSS={block:this.api.styles.block,wrapper:"ce-header"},this._settings=t,this._data=this.normalizeData(e),this._element=this.getTag()}normalizeData(e){const t={};return typeof e!="object"&&(e={}),t.text=e.text||"",t.level=parseInt(e.level)||this.defaultLevel.number,t}render(){return this._element}renderSettings(){return this.levels.map(e=>({icon:e.svg,label:this.api.i18n.t(`Heading ${e.number}`),onActivate:()=>this.setLevel(e.number),closeOnActivate:!0,isActive:this.currentLevel.number===e.number}))}setLevel(e){this.data={level:e,text:this.data.text}}merge(e){const t={text:this.data.text+e.text,level:this.data.level};this.data=t}validate(e){return e.text.trim()!==""}save(e){return{text:e.innerHTML,level:this.currentLevel.number}}static get conversionConfig(){return{export:"text",import:"text"}}static get sanitize(){return{level:!1,text:{}}}static get isReadOnlySupported(){return!0}get data(){return this._data.text=this._element.innerHTML,this._data.level=this.currentLevel.number,this._data}set data(e){if(this._data=this.normalizeData(e),e.level!==void 0&&this._element.parentNode){const t=this.getTag();t.innerHTML=this._element.innerHTML,this._element.parentNode.replaceChild(t,this._element),this._element=t}e.text!==void 0&&(this._element.innerHTML=this._data.text||"")}getTag(){const e=document.createElement(this.currentLevel.tag);return e.innerHTML=this._data.text||"",e.classList.add(this._CSS.wrapper),e.contentEditable=this.readOnly?"false":"true",e.dataset.placeholder=this.api.i18n.t(this._settings.placeholder||""),e}get currentLevel(){let e=this.levels.find(t=>t.number===this._data.level);return e||(e=this.defaultLevel),e}get defaultLevel(){if(this._settings.defaultLevel){const e=this.levels.find(t=>t.number===this._settings.defaultLevel);if(e)return e;console.warn("(ง'̀-'́)ง Heading Tool: the default level specified was not found in available levels")}return this.levels[1]}get levels(){const e=[{number:1,tag:"H1",svg:r},{number:2,tag:"H2",svg:o},{number:3,tag:"H3",svg:a},{number:4,tag:"H4",svg:h},{number:5,tag:"H5",svg:d},{number:6,tag:"H6",svg:u}];return this._settings.levels?e.filter(t=>this._settings.levels.includes(t.number)):e}onPaste(e){const t=e.detail.data;let s=this.defaultLevel.number;switch(t.tagName){case"H1":s=1;break;case"H2":s=2;break;case"H3":s=3;break;case"H4":s=4;break;case"H5":s=5;break;case"H6":s=6;break}this._settings.levels&&(s=this._settings.levels.reduce((i,l)=>Math.abs(l-s)<Math.abs(i-s)?l:i)),this.data={level:s,text:t.innerHTML}}static get pasteConfig(){return{tags:["H1","H2","H3","H4","H5","H6"]}}static get toolbox(){return{icon:g,title:"Heading"}}}return c});
|
||||
32
app/src/main/assets/editor/image.umd.js
Normal file
32
app/src/main/assets/editor/image.umd.js
Normal file
File diff suppressed because one or more lines are too long
1225
app/src/main/assets/editor/index.html
Normal file
1225
app/src/main/assets/editor/index.html
Normal file
File diff suppressed because it is too large
Load Diff
2
app/src/main/assets/editor/inline-code.umd.js
Normal file
2
app/src/main/assets/editor/inline-code.umd.js
Normal file
@@ -0,0 +1,2 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".inline-code{background:rgba(250,239,240,.78);color:#b44437;padding:3px 4px;border-radius:5px;margin:0 1px;font-family:inherit;font-size:.86em;font-weight:500;letter-spacing:.3px}")),document.head.appendChild(e)}}catch(n){console.error("vite-plugin-css-injected-by-js",n)}})();
|
||||
(function(i,s){typeof exports=="object"&&typeof module<"u"?module.exports=s():typeof define=="function"&&define.amd?define(s):(i=typeof globalThis<"u"?globalThis:i||self,i.InlineCode=s())})(this,function(){"use strict";const i="",s='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M9.5 8L6.11524 11.8683C6.04926 11.9437 6.04926 12.0563 6.11524 12.1317L9.5 16"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M15 8L18.3848 11.8683C18.4507 11.9437 18.4507 12.0563 18.3848 12.1317L15 16"/></svg>';class n{static get CSS(){return"inline-code"}constructor({api:t}){this.api=t,this.button=null,this.tag="CODE",this.iconClasses={base:this.api.styles.inlineToolButton,active:this.api.styles.inlineToolButtonActive}}static get isInline(){return!0}render(){return this.button=document.createElement("button"),this.button.type="button",this.button.classList.add(this.iconClasses.base),this.button.innerHTML=this.toolboxIcon,this.button}surround(t){if(!t)return;let e=this.api.selection.findParentTag(this.tag,n.CSS);e?this.unwrap(e):this.wrap(t)}wrap(t){let e=document.createElement(this.tag);e.classList.add(n.CSS),e.appendChild(t.extractContents()),t.insertNode(e),this.api.selection.expandToTag(e)}unwrap(t){this.api.selection.expandToTag(t);let e=window.getSelection(),o=e.getRangeAt(0),a=o.extractContents();t.parentNode.removeChild(t),o.insertNode(a),e.removeAllRanges(),e.addRange(o)}checkState(){const t=this.api.selection.findParentTag(this.tag,n.CSS);this.button.classList.toggle(this.iconClasses.active,!!t)}get toolboxIcon(){return s}static get sanitize(){return{code:{class:n.CSS}}}}return n});
|
||||
2
app/src/main/assets/editor/list.umd.js
Normal file
2
app/src/main/assets/editor/list.umd.js
Normal file
File diff suppressed because one or more lines are too long
2
app/src/main/assets/editor/marker.umd.js
Normal file
2
app/src/main/assets/editor/marker.umd.js
Normal file
@@ -0,0 +1,2 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".cdx-marker{background:rgba(245,235,111,.29);padding:3px 0}")),document.head.appendChild(e)}}catch(d){console.error("vite-plugin-css-injected-by-js",d)}})();
|
||||
(function(i,s){typeof exports=="object"&&typeof module<"u"?module.exports=s():typeof define=="function"&&define.amd?define(s):(i=typeof globalThis<"u"?globalThis:i||self,i.Marker=s())})(this,function(){"use strict";const i="",s='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-width="2" d="M11.3536 9.31802L12.7678 7.90381C13.5488 7.12276 14.8151 7.12276 15.5962 7.90381C16.3772 8.68486 16.3772 9.95119 15.5962 10.7322L14.182 12.1464M11.3536 9.31802L7.96729 12.7043C7.40889 13.2627 7.02827 13.9739 6.8734 14.7482L6.69798 15.6253C6.55804 16.325 7.17496 16.942 7.87468 16.802L8.75176 16.6266C9.52612 16.4717 10.2373 16.0911 10.7957 15.5327L14.182 12.1464M11.3536 9.31802L14.182 12.1464"/><line x1="15" x2="19" y1="17" y2="17" stroke="currentColor" stroke-linecap="round" stroke-width="2"/></svg>';class n{static get CSS(){return"cdx-marker"}constructor({api:t}){this.api=t,this.button=null,this.tag="MARK",this.iconClasses={base:this.api.styles.inlineToolButton,active:this.api.styles.inlineToolButtonActive}}static get isInline(){return!0}render(){return this.button=document.createElement("button"),this.button.type="button",this.button.classList.add(this.iconClasses.base),this.button.innerHTML=this.toolboxIcon,this.button}surround(t){if(!t)return;let e=this.api.selection.findParentTag(this.tag,n.CSS);e?this.unwrap(e):this.wrap(t)}wrap(t){let e=document.createElement(this.tag);e.classList.add(n.CSS),e.appendChild(t.extractContents()),t.insertNode(e),this.api.selection.expandToTag(e)}unwrap(t){this.api.selection.expandToTag(t);let e=window.getSelection(),o=e.getRangeAt(0),a=o.extractContents();t.parentNode.removeChild(t),o.insertNode(a),e.removeAllRanges(),e.addRange(o)}checkState(){const t=this.api.selection.findParentTag(this.tag,n.CSS);this.button.classList.toggle(this.iconClasses.active,!!t)}get toolboxIcon(){return s}static get sanitize(){return{mark:{class:n.CSS}}}}return n});
|
||||
2
app/src/main/assets/editor/quote.umd.js
Normal file
2
app/src/main/assets/editor/quote.umd.js
Normal file
@@ -0,0 +1,2 @@
|
||||
(function(){"use strict";try{if(typeof document<"u"){var t=document.createElement("style");t.appendChild(document.createTextNode(".cdx-quote-icon svg{transform:rotate(180deg)}.cdx-quote{margin:0}.cdx-quote__text{min-height:158px;margin-bottom:10px}.cdx-quote [contentEditable=true][data-placeholder]:before{position:absolute;content:attr(data-placeholder);color:#707684;font-weight:400;opacity:0}.cdx-quote [contentEditable=true][data-placeholder]:empty:before{opacity:1}.cdx-quote [contentEditable=true][data-placeholder]:empty:focus:before{opacity:0}.cdx-quote-settings{display:flex}.cdx-quote-settings .cdx-settings-button{width:50%}")),document.head.appendChild(t)}}catch(e){console.error("vite-plugin-css-injected-by-js",e)}})();
|
||||
(function(s,o){typeof exports=="object"&&typeof module<"u"?module.exports=o():typeof define=="function"&&define.amd?define(o):(s=typeof globalThis<"u"?globalThis:s||self,s.Quote=o())})(this,function(){"use strict";const s="",o='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M18 7L6 7"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M18 17H6"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 12L8 12"/></svg>',c='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M17 7L5 7"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M17 17H5"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M13 12L5 12"/></svg>',l='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 10.8182L9 10.8182C8.80222 10.8182 8.60888 10.7649 8.44443 10.665C8.27998 10.5651 8.15181 10.4231 8.07612 10.257C8.00043 10.0909 7.98063 9.90808 8.01922 9.73174C8.0578 9.55539 8.15304 9.39341 8.29289 9.26627C8.43275 9.13913 8.61093 9.05255 8.80491 9.01747C8.99889 8.98239 9.19996 9.00039 9.38268 9.0692C9.56541 9.13801 9.72159 9.25453 9.83147 9.40403C9.94135 9.55353 10 9.72929 10 9.90909L10 12.1818C10 12.664 9.78929 13.1265 9.41421 13.4675C9.03914 13.8084 8.53043 14 8 14"/><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 10.8182L15 10.8182C14.8022 10.8182 14.6089 10.7649 14.4444 10.665C14.28 10.5651 14.1518 10.4231 14.0761 10.257C14.0004 10.0909 13.9806 9.90808 14.0192 9.73174C14.0578 9.55539 14.153 9.39341 14.2929 9.26627C14.4327 9.13913 14.6109 9.05255 14.8049 9.01747C14.9989 8.98239 15.2 9.00039 15.3827 9.0692C15.5654 9.13801 15.7216 9.25453 15.8315 9.40403C15.9414 9.55353 16 9.72929 16 9.90909L16 12.1818C16 12.664 15.7893 13.1265 15.4142 13.4675C15.0391 13.8084 14.5304 14 14 14"/></svg>';class i{static get isReadOnlySupported(){return!0}static get toolbox(){return{icon:l,title:"Quote"}}static get contentless(){return!0}static get enableLineBreaks(){return!0}static get DEFAULT_QUOTE_PLACEHOLDER(){return"Enter a quote"}static get DEFAULT_CAPTION_PLACEHOLDER(){return"Enter a caption"}static get ALIGNMENTS(){return{left:"left",center:"center"}}static get DEFAULT_ALIGNMENT(){return i.ALIGNMENTS.left}static get conversionConfig(){return{import:"text",export:function(t){return t.caption?`${t.text} — ${t.caption}`:t.text}}}get CSS(){return{baseClass:this.api.styles.block,wrapper:"cdx-quote",text:"cdx-quote__text",input:this.api.styles.input,caption:"cdx-quote__caption"}}get settings(){return[{name:"left",icon:c},{name:"center",icon:o}]}constructor({data:t,config:e,api:n,readOnly:r}){const{ALIGNMENTS:a,DEFAULT_ALIGNMENT:d}=i;this.api=n,this.readOnly=r,this.quotePlaceholder=e.quotePlaceholder||i.DEFAULT_QUOTE_PLACEHOLDER,this.captionPlaceholder=e.captionPlaceholder||i.DEFAULT_CAPTION_PLACEHOLDER,this.data={text:t.text||"",caption:t.caption||"",alignment:Object.values(a).includes(t.alignment)&&t.alignment||e.defaultAlignment||d}}render(){const t=this._make("blockquote",[this.CSS.baseClass,this.CSS.wrapper]),e=this._make("div",[this.CSS.input,this.CSS.text],{contentEditable:!this.readOnly,innerHTML:this.data.text}),n=this._make("div",[this.CSS.input,this.CSS.caption],{contentEditable:!this.readOnly,innerHTML:this.data.caption});return e.dataset.placeholder=this.quotePlaceholder,n.dataset.placeholder=this.captionPlaceholder,t.appendChild(e),t.appendChild(n),t}save(t){const e=t.querySelector(`.${this.CSS.text}`),n=t.querySelector(`.${this.CSS.caption}`);return Object.assign(this.data,{text:e.innerHTML,caption:n.innerHTML})}static get sanitize(){return{text:{br:!0},caption:{br:!0},alignment:{}}}renderSettings(){const t=e=>e[0].toUpperCase()+e.substr(1);return this.settings.map(e=>({icon:e.icon,label:this.api.i18n.t(`Align ${t(e.name)}`),onActivate:()=>this._toggleTune(e.name),isActive:this.data.alignment===e.name,closeOnActivate:!0}))}_toggleTune(t){this.data.alignment=t}_make(t,e=null,n={}){const r=document.createElement(t);Array.isArray(e)?r.classList.add(...e):e&&r.classList.add(e);for(const a in n)r[a]=n[a];return r}}return i});
|
||||
253
app/src/main/assets/editor/style.css
Normal file
253
app/src/main/assets/editor/style.css
Normal file
@@ -0,0 +1,253 @@
|
||||
:root {
|
||||
--bg-color: #ffffff;
|
||||
--text-color: #000000;
|
||||
--code-bg: #f6f8fa;
|
||||
--border-color: #e5e7eb;
|
||||
--table-header-bg: #f9fafb;
|
||||
--scrollbar-track: #f1f1f1;
|
||||
--scrollbar-thumb: #c1c1c1;
|
||||
--scrollbar-thumb-hover: #a1a1a1;
|
||||
--placeholder-color: #9ca3af;
|
||||
--quote-color: #4b5563;
|
||||
--caption-color: #6b7280;
|
||||
}
|
||||
|
||||
html.dark-mode {
|
||||
--bg-color: #1a1a1a;
|
||||
--text-color: #e0e0e0;
|
||||
--code-bg: #2d2d2d;
|
||||
--border-color: #404040;
|
||||
--table-header-bg: #2a2a2a;
|
||||
--scrollbar-track: #2d2d2d;
|
||||
--scrollbar-thumb: #555555;
|
||||
--scrollbar-thumb-hover: #666666;
|
||||
--placeholder-color: #6b7280;
|
||||
--quote-color: #9ca3af;
|
||||
--caption-color: #9ca3af;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
/* Editor.js container */
|
||||
#editorjs {
|
||||
min-height: calc(100vh - 100px);
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.ce-block__content {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.ce-toolbar__content {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Editor.js table styles */
|
||||
.tc-table {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.tc-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tc-table__cell {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 12px;
|
||||
min-width: 50px;
|
||||
color: var(--text-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tc-table__header {
|
||||
background: var(--table-header-bg);
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Editor.js code block */
|
||||
.ce-code {
|
||||
background: var(--code-bg);
|
||||
color: var(--text-color);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Editor.js quote */
|
||||
.ce-quote {
|
||||
border-left: 3px solid var(--border-color);
|
||||
padding-left: 16px;
|
||||
margin: 0.5em 0;
|
||||
color: var(--quote-color);
|
||||
}
|
||||
|
||||
/* Editor.js checklist */
|
||||
.ce-checklist__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.ce-checklist__item-checkbox {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Undo/Redo floating buttons */
|
||||
.undo-redo-container {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.undo-btn, .redo-btn {
|
||||
pointer-events: auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: color-mix(in srgb, var(--bg-color) 85%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
opacity: 0.65;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, background 0.2s;
|
||||
color: var(--text-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
html.dark-mode .undo-btn,
|
||||
html.dark-mode .redo-btn {
|
||||
background: color-mix(in srgb, var(--bg-color) 80%, transparent);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.undo-btn:active, .redo-btn:active {
|
||||
opacity: 0.9;
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
.undo-btn[disabled], .redo-btn[disabled] {
|
||||
opacity: 0.15;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Editor.js delimiter */
|
||||
.ce-delimiter {
|
||||
margin: 1.5em 0;
|
||||
border: none;
|
||||
border-top: 2px solid var(--border-color);
|
||||
padding: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ce-delimiter::before,
|
||||
.ce-delimiter::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ce-delimiter * {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ce-delimiter__content {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ce-delimiter__content * {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Editor.js header styles */
|
||||
.ce-header[data-item='1'],
|
||||
.ce-header[style*="font-size: 32px"],
|
||||
.ce-header h1,
|
||||
[data-tool="header1"] .ce-header {
|
||||
font-size: 2em;
|
||||
font-weight: 700;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
.ce-header[data-item='2'],
|
||||
.ce-header[style*="font-size: 24px"],
|
||||
.ce-header h2,
|
||||
[data-tool="header2"] .ce-header {
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
margin: 0.75em 0;
|
||||
}
|
||||
|
||||
.ce-header[data-item='3'],
|
||||
.ce-header[style*="font-size: 20px"],
|
||||
.ce-header h3,
|
||||
[data-tool="header3"] .ce-header {
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
margin: 0.83em 0;
|
||||
}
|
||||
|
||||
/* Custom File Tool (Attachments) */
|
||||
.custom-file-tool {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
background-color: var(--code-bg);
|
||||
}
|
||||
|
||||
|
||||
2
app/src/main/assets/editor/table.umd.js
Normal file
2
app/src/main/assets/editor/table.umd.js
Normal file
File diff suppressed because one or more lines are too long
5
app/src/main/java/com/docmost/app/DocmostApp.kt
Normal file
5
app/src/main/java/com/docmost/app/DocmostApp.kt
Normal file
@@ -0,0 +1,5 @@
|
||||
package com.docmost.app
|
||||
|
||||
import android.app.Application
|
||||
|
||||
class DocmostApp : Application()
|
||||
368
app/src/main/java/com/docmost/app/api/CachedApi.kt
Normal file
368
app/src/main/java/com/docmost/app/api/CachedApi.kt
Normal file
@@ -0,0 +1,368 @@
|
||||
package com.docmost.app.api
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.docmost.app.utils.NetworkConnectivityObserver
|
||||
import com.docmost.app.data.PageCacheManager
|
||||
import com.docmost.app.data.SpaceCacheManager
|
||||
import com.docmost.app.data.SyncManager
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class CachedApi(
|
||||
private val api: DocmostApi,
|
||||
private val pageCache: PageCacheManager,
|
||||
private val spaceCache: SpaceCacheManager,
|
||||
private val syncManager: SyncManager,
|
||||
private val networkObserver: NetworkConnectivityObserver
|
||||
) {
|
||||
var forcedOffline: Boolean = false
|
||||
|
||||
val isOffline: Boolean
|
||||
get() = forcedOffline || networkObserver.isOffline()
|
||||
|
||||
private fun <T> cacheOrThrow(block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: Exception) {
|
||||
throw OfflineNotAvailableException("No disponible sin conexión: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spaces ──
|
||||
|
||||
suspend fun getSpaces(): List<Space> {
|
||||
if (isOffline) {
|
||||
val cached = spaceCache.getCachedSpaces()
|
||||
if (cached.isNotEmpty()) return cached
|
||||
throw OfflineNotAvailableException("No hay espacios en caché")
|
||||
}
|
||||
val spaces = api.getSpaces()
|
||||
spaceCache.cacheSpaces(spaces)
|
||||
return spaces
|
||||
}
|
||||
|
||||
suspend fun createSpace(name: String, slug: String, description: String?): Space {
|
||||
if (isOffline) {
|
||||
val tempId = "temp_${System.currentTimeMillis()}"
|
||||
syncManager.enqueueCreateSpace(name, slug, description, tempId)
|
||||
val localSpace = Space(
|
||||
id = tempId,
|
||||
name = name,
|
||||
description = description
|
||||
)
|
||||
spaceCache.cacheSpace(localSpace)
|
||||
return localSpace
|
||||
}
|
||||
val space = api.createSpace(name, slug, description)
|
||||
spaceCache.cacheSpace(space)
|
||||
return space
|
||||
}
|
||||
|
||||
suspend fun updateSpace(spaceId: String, name: String?, description: String?): Space {
|
||||
if (isOffline) {
|
||||
syncManager.enqueueUpdateSpace(spaceId, name, description)
|
||||
val cached = spaceCache.getCachedSpace(spaceId)
|
||||
if (cached != null) {
|
||||
val updated = cached.copy(
|
||||
name = name ?: cached.name,
|
||||
description = description ?: cached.description
|
||||
)
|
||||
spaceCache.cacheSpace(updated)
|
||||
return updated
|
||||
}
|
||||
throw OfflineNotAvailableException("Espacio no encontrado en caché")
|
||||
}
|
||||
val space = api.updateSpace(spaceId, name, description)
|
||||
spaceCache.cacheSpace(space)
|
||||
return space
|
||||
}
|
||||
|
||||
suspend fun deleteSpace(spaceId: String) {
|
||||
if (isOffline) {
|
||||
syncManager.enqueueDeleteSpace(spaceId)
|
||||
spaceCache.removeCachedSpace(spaceId)
|
||||
return
|
||||
}
|
||||
api.deleteSpace(spaceId)
|
||||
spaceCache.removeCachedSpace(spaceId)
|
||||
}
|
||||
|
||||
suspend fun uploadSpaceIcon(spaceId: String, imageUri: Uri, context: Context): String {
|
||||
if (isOffline) {
|
||||
throw OfflineNotAvailableException("Subir iconos no disponible sin conexión")
|
||||
}
|
||||
return api.uploadSpaceIcon(spaceId, imageUri, context)
|
||||
}
|
||||
|
||||
suspend fun removeSpaceIcon(spaceId: String) {
|
||||
if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión")
|
||||
api.removeSpaceIcon(spaceId)
|
||||
}
|
||||
|
||||
// ── Pages ──
|
||||
|
||||
suspend fun getSidebarPages(spaceId: String, pageId: String? = null): List<Page> {
|
||||
if (isOffline) {
|
||||
val cached = pageCache.getCachedPagesForSpace(spaceId)
|
||||
if (cached != null) return cached
|
||||
return emptyList()
|
||||
}
|
||||
val pages = api.getSidebarPages(spaceId, pageId)
|
||||
pageCache.cachePagesForSpace(spaceId, pages)
|
||||
return pages
|
||||
}
|
||||
|
||||
suspend fun getPageInfo(pageId: String): Page {
|
||||
if (isOffline) {
|
||||
val cached = pageCache.getCachedPage(pageId)
|
||||
if (cached != null) return cached
|
||||
throw OfflineNotAvailableException("Página no encontrada en caché")
|
||||
}
|
||||
val page = api.getPageInfo(pageId)
|
||||
pageCache.cachePage(page)
|
||||
return page
|
||||
}
|
||||
|
||||
suspend fun createPage(title: String, spaceId: String): Page {
|
||||
if (isOffline) {
|
||||
val tempId = "temp_${System.currentTimeMillis()}"
|
||||
syncManager.enqueueCreatePage(title, spaceId, tempId)
|
||||
val localPage = Page(
|
||||
id = tempId,
|
||||
title = title,
|
||||
spaceId = spaceId
|
||||
)
|
||||
pageCache.cachePage(localPage)
|
||||
val existing = pageCache.getCachedPagesForSpace(spaceId)?.toMutableList() ?: mutableListOf()
|
||||
existing.add(localPage)
|
||||
pageCache.cachePagesForSpace(spaceId, existing)
|
||||
return localPage
|
||||
}
|
||||
val page = api.createPage(title, spaceId)
|
||||
pageCache.cachePage(page)
|
||||
val existing = pageCache.getCachedPagesForSpace(spaceId)?.toMutableList() ?: mutableListOf()
|
||||
existing.add(page)
|
||||
pageCache.cachePagesForSpace(spaceId, existing)
|
||||
return page
|
||||
}
|
||||
|
||||
suspend fun updatePage(pageId: String, content: String?, title: String?, icon: String? = null, isJson: Boolean = false): Page {
|
||||
if (isOffline) {
|
||||
syncManager.enqueueUpdatePage(pageId, content ?: "", title ?: "", icon)
|
||||
val cached = pageCache.getCachedPage(pageId)
|
||||
if (cached != null) {
|
||||
val updated = cached.copy(
|
||||
title = title ?: cached.title,
|
||||
icon = icon ?: cached.icon,
|
||||
content = content ?: cached.content
|
||||
)
|
||||
pageCache.cachePage(updated)
|
||||
return updated
|
||||
}
|
||||
throw OfflineNotAvailableException("Página no encontrada en caché")
|
||||
}
|
||||
val page = api.updatePage(pageId, content, title, icon, isJson)
|
||||
pageCache.cachePage(page)
|
||||
return page
|
||||
}
|
||||
|
||||
suspend fun deletePage(pageId: String, spaceId: String? = null) {
|
||||
if (isOffline) {
|
||||
syncManager.enqueueDeletePage(pageId)
|
||||
pageCache.removeFromCache(pageId)
|
||||
if (spaceId != null) {
|
||||
val cached = pageCache.getCachedPagesForSpace(spaceId)?.filter { it.id != pageId }
|
||||
if (cached != null) pageCache.cachePagesForSpace(spaceId, cached)
|
||||
}
|
||||
return
|
||||
}
|
||||
api.deletePage(pageId)
|
||||
pageCache.removeFromCache(pageId)
|
||||
if (spaceId != null) {
|
||||
val cached = pageCache.getCachedPagesForSpace(spaceId)?.filter { it.id != pageId }
|
||||
if (cached != null) pageCache.cachePagesForSpace(spaceId, cached)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun duplicatePage(pageId: String, targetSpaceId: String? = null): Page {
|
||||
if (isOffline) throw OfflineNotAvailableException("Duplicar no disponible sin conexión")
|
||||
|
||||
val page = api.duplicatePage(pageId, targetSpaceId)
|
||||
pageCache.cachePage(page)
|
||||
return page
|
||||
}
|
||||
|
||||
suspend fun movePageToSpace(pageId: String, spaceId: String) {
|
||||
if (isOffline) {
|
||||
syncManager.enqueueUpdatePage(pageId, "", "", null)
|
||||
return
|
||||
}
|
||||
api.movePageToSpace(pageId, spaceId)
|
||||
}
|
||||
|
||||
suspend fun movePageToPage(pageId: String, parentPageId: String?, position: String) {
|
||||
if (isOffline) {
|
||||
syncManager.enqueueUpdatePage(pageId, "", "", null)
|
||||
return
|
||||
}
|
||||
api.movePageToPage(pageId, parentPageId, position)
|
||||
}
|
||||
|
||||
// ── Recent ──
|
||||
|
||||
suspend fun getRecentPages(): List<Page> {
|
||||
if (isOffline) throw OfflineNotAvailableException("Recientes no disponible sin conexión")
|
||||
return api.getRecentPages()
|
||||
}
|
||||
|
||||
// ── Shares ──
|
||||
|
||||
suspend fun getShares(): List<Share> {
|
||||
if (isOffline) throw OfflineNotAvailableException("Compartir no disponible sin conexión")
|
||||
return api.getShares()
|
||||
}
|
||||
|
||||
suspend fun createShare(pageId: String): Share {
|
||||
if (isOffline) throw OfflineNotAvailableException("Crear enlaces no disponible sin conexión")
|
||||
return api.createShare(pageId)
|
||||
}
|
||||
|
||||
suspend fun deleteShare(shareId: String) {
|
||||
if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión")
|
||||
api.deleteShare(shareId)
|
||||
}
|
||||
|
||||
// ── Comments ──
|
||||
|
||||
suspend fun getComments(pageId: String): List<Comment> {
|
||||
if (isOffline) throw OfflineNotAvailableException("Comentarios no disponible sin conexión")
|
||||
return api.getComments(pageId)
|
||||
}
|
||||
|
||||
suspend fun createComment(pageId: String, content: String): Comment {
|
||||
if (isOffline) throw OfflineNotAvailableException("Comentarios no disponible sin conexión")
|
||||
return api.createComment(pageId, content)
|
||||
}
|
||||
|
||||
suspend fun updateComment(commentId: String, content: String): Comment {
|
||||
if (isOffline) throw OfflineNotAvailableException("Comentarios no disponible sin conexión")
|
||||
return api.updateComment(commentId, content)
|
||||
}
|
||||
|
||||
suspend fun deleteComment(commentId: String) {
|
||||
if (isOffline) throw OfflineNotAvailableException("Comentarios no disponible sin conexión")
|
||||
api.deleteComment(commentId)
|
||||
}
|
||||
|
||||
// ── History ──
|
||||
|
||||
suspend fun getPageHistory(pageId: String): List<PageHistory> {
|
||||
if (isOffline) throw OfflineNotAvailableException("Historial no disponible sin conexión")
|
||||
return api.getPageHistory(pageId)
|
||||
}
|
||||
|
||||
suspend fun getPageHistoryInfo(pageId: String, historyId: String): PageHistory {
|
||||
if (isOffline) throw OfflineNotAvailableException("Historial no disponible sin conexión")
|
||||
return api.getPageHistoryInfo(pageId, historyId)
|
||||
}
|
||||
|
||||
// ── User ──
|
||||
|
||||
suspend fun getCurrentUser(): User {
|
||||
if (isOffline) throw OfflineNotAvailableException("Usuario no disponible sin conexión")
|
||||
return api.getCurrentUser()
|
||||
}
|
||||
|
||||
suspend fun updateUser(name: String?): User {
|
||||
if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión")
|
||||
return api.updateUser(name)
|
||||
}
|
||||
|
||||
// ── Search ──
|
||||
|
||||
suspend fun searchPages(query: String): List<SearchResult> {
|
||||
if (isOffline) throw OfflineNotAvailableException("Búsqueda no disponible sin conexión")
|
||||
return api.searchPages(query)
|
||||
}
|
||||
|
||||
// ── Attachments ──
|
||||
|
||||
suspend fun uploadAttachment(pageId: String, fileUri: Uri, context: Context): Attachment {
|
||||
if (isOffline) {
|
||||
val fileName = getFileNameFromUri(context, fileUri) ?: "file"
|
||||
val mimeType = context.contentResolver.getType(fileUri) ?: "application/octet-stream"
|
||||
val pendingDir = File(context.cacheDir, "attachments_pending/$pageId")
|
||||
pendingDir.mkdirs()
|
||||
val localFile = File(pendingDir, fileName)
|
||||
try {
|
||||
context.contentResolver.openInputStream(fileUri)?.use { input ->
|
||||
localFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw IOException("No se pudo guardar el archivo localmente: ${e.message}")
|
||||
}
|
||||
syncManager.enqueueUploadAttachment(pageId, localFile.absolutePath, fileName, mimeType)
|
||||
|
||||
return Attachment(
|
||||
id = "pending_${System.currentTimeMillis()}",
|
||||
fileName = fileName,
|
||||
fileSize = localFile.length(),
|
||||
mimeType = mimeType,
|
||||
url = localFile.toURI().toString(),
|
||||
pageId = pageId
|
||||
)
|
||||
}
|
||||
return api.uploadAttachment(pageId, fileUri, context)
|
||||
}
|
||||
|
||||
suspend fun getPageAttachments(pageId: String): List<Attachment> {
|
||||
if (isOffline) throw OfflineNotAvailableException("Archivos adjuntos no disponible sin conexión")
|
||||
return api.getPageAttachments(pageId)
|
||||
}
|
||||
|
||||
suspend fun getPageContent(pageId: String): PageContent {
|
||||
if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión")
|
||||
return api.getPageContent(pageId)
|
||||
}
|
||||
|
||||
// ── Trash ──
|
||||
|
||||
suspend fun getDeletedPages(spaceId: String): List<DeletedPage> {
|
||||
if (isOffline) throw OfflineNotAvailableException("Papelera no disponible sin conexión")
|
||||
return api.getDeletedPages(spaceId)
|
||||
}
|
||||
|
||||
suspend fun restorePage(pageId: String, spaceId: String? = null) {
|
||||
if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión")
|
||||
api.restorePage(pageId, spaceId)
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
suspend fun getPageHtml(pageId: String): String {
|
||||
if (isOffline) throw OfflineNotAvailableException("No disponible sin conexión")
|
||||
return api.getPageHtml(pageId)
|
||||
}
|
||||
|
||||
fun getAttachmentUrl(attachmentType: String, fileName: String): String {
|
||||
return api.getAttachmentUrl(attachmentType, fileName)
|
||||
}
|
||||
|
||||
private fun getFileNameFromUri(context: Context, uri: Uri): String? {
|
||||
var fileName: String? = null
|
||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
val displayNameIndex = it.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
|
||||
if (displayNameIndex >= 0) {
|
||||
fileName = it.getString(displayNameIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileName ?: uri.lastPathSegment
|
||||
}
|
||||
}
|
||||
488
app/src/main/java/com/docmost/app/api/DocmostApi.kt
Normal file
488
app/src/main/java/com/docmost/app/api/DocmostApi.kt
Normal file
@@ -0,0 +1,488 @@
|
||||
package com.docmost.app.api
|
||||
|
||||
import android.util.Log
|
||||
import com.docmost.app.BuildConfig
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class DocmostApi(private val baseUrl: String) {
|
||||
|
||||
private val gson = Gson()
|
||||
private val jsonMediaType = "application/json".toMediaType()
|
||||
|
||||
private var authToken: String? = null
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
fun setAuthToken(token: String?) {
|
||||
authToken = token
|
||||
}
|
||||
|
||||
private fun buildUrl(path: String): String {
|
||||
val url = baseUrl.trimEnd('/')
|
||||
return if (path.startsWith("/")) "$url$path" else "$url/$path"
|
||||
}
|
||||
|
||||
private fun post(path: String, body: Any? = null): String {
|
||||
val url = buildUrl(path)
|
||||
val jsonBody = if (body != null) gson.toJson(body) else "{}"
|
||||
val requestBody = jsonBody.toRequestBody(jsonMediaType)
|
||||
if (BuildConfig.DEBUG) Log.d("DocmostAPI", "POST $url")
|
||||
|
||||
val builder = Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
|
||||
authToken?.let {
|
||||
builder.addHeader("Cookie", "authToken=$it")
|
||||
}
|
||||
|
||||
val request = builder.build()
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
if (BuildConfig.DEBUG) Log.d("DocmostAPI", "HTTP ${response.code}")
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
val message = try {
|
||||
val err = gson.fromJson(responseBody, ApiError::class.java)
|
||||
err.message
|
||||
} catch (e: Exception) {
|
||||
"HTTP ${response.code}"
|
||||
}
|
||||
throw IOException(message)
|
||||
}
|
||||
|
||||
return responseBody
|
||||
}
|
||||
|
||||
suspend fun login(email: String, password: String): String = withContext(Dispatchers.IO) {
|
||||
val url = buildUrl("/api/auth/login")
|
||||
val jsonBody = gson.toJson(LoginRequest(email, password))
|
||||
val requestBody = jsonBody.toRequestBody(jsonMediaType)
|
||||
if (BuildConfig.DEBUG) Log.d("DocmostAPI", "POST $url")
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
if (BuildConfig.DEBUG) Log.d("DocmostAPI", "HTTP ${response.code}")
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
val message = try {
|
||||
val err = gson.fromJson(responseBody, ApiError::class.java)
|
||||
err.message
|
||||
} catch (e: Exception) {
|
||||
"Login failed (HTTP ${response.code})"
|
||||
}
|
||||
throw IOException(message)
|
||||
}
|
||||
|
||||
val cookies = response.headers("Set-Cookie")
|
||||
var token: String? = null
|
||||
for (cookie in cookies) {
|
||||
if (cookie.startsWith("authToken=", ignoreCase = true)) {
|
||||
val parts = cookie.split(";")[0].trim()
|
||||
token = parts.removePrefix("authToken=")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (token.isNullOrBlank()) {
|
||||
throw IOException("No auth token received")
|
||||
}
|
||||
|
||||
authToken = token
|
||||
token
|
||||
}
|
||||
|
||||
suspend fun getSpaces(): List<Space> = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/spaces")
|
||||
val type = object : TypeToken<DocmostResponse<SpacesData>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<SpacesData>>(json, type)
|
||||
val items = response.data?.items ?: emptyList()
|
||||
items
|
||||
}
|
||||
|
||||
suspend fun getSidebarPages(spaceId: String, pageId: String? = null): List<Page> = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/pages/sidebar-pages", SidebarPagesRequest(spaceId = spaceId, pageId = pageId))
|
||||
val type = object : TypeToken<DocmostResponse<PagesData>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<PagesData>>(json, type)
|
||||
response.data?.items ?: emptyList()
|
||||
}
|
||||
|
||||
private suspend fun fetchPageChildren(spaceId: String, parentPage: Page, allPages: MutableList<Page>) {
|
||||
if (parentPage.hasChildren) {
|
||||
val children = getSidebarPages(spaceId, parentPage.id)
|
||||
allPages.addAll(children)
|
||||
children.forEach { child ->
|
||||
fetchPageChildren(spaceId, child, allPages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAllPagesInSpace(spaceId: String): List<Page> = withContext(Dispatchers.IO) {
|
||||
val allPages = mutableListOf<Page>()
|
||||
val rootPages = getSidebarPages(spaceId)
|
||||
allPages.addAll(rootPages)
|
||||
|
||||
rootPages.forEach { page ->
|
||||
fetchPageChildren(spaceId, page, allPages)
|
||||
}
|
||||
|
||||
allPages
|
||||
}
|
||||
|
||||
suspend fun getPageInfo(pageId: String, format: String = "json"): Page = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/pages/info", PageInfoRequest(pageId, format))
|
||||
val type = object : TypeToken<DocmostResponse<Page>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<Page>>(json, type)
|
||||
response.data ?: throw IOException("Page not found in response")
|
||||
}
|
||||
|
||||
suspend fun getPageHtml(pageId: String): String = withContext(Dispatchers.IO) {
|
||||
val page = getPageInfo(pageId, "html")
|
||||
val content = page.content as? String ?: throw IOException("Page content is not a string")
|
||||
content
|
||||
}
|
||||
|
||||
fun getAttachmentUrl(attachmentType: String, fileName: String): String {
|
||||
return buildUrl("/api/attachments/img/$attachmentType/$fileName")
|
||||
}
|
||||
|
||||
suspend fun updateSpace(spaceId: String, name: String?, description: String?): Space =
|
||||
withContext(Dispatchers.IO) {
|
||||
val json = post(
|
||||
"/api/spaces/update",
|
||||
SpaceUpdateRequest(
|
||||
spaceId = spaceId,
|
||||
name = name,
|
||||
description = description
|
||||
)
|
||||
)
|
||||
val type = object : TypeToken<DocmostResponse<Space>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<Space>>(json, type)
|
||||
response.data ?: throw IOException("Space update response missing data")
|
||||
}
|
||||
|
||||
suspend fun uploadSpaceIcon(spaceId: String, imageUri: Uri, context: Context): String =
|
||||
withContext(Dispatchers.IO) {
|
||||
val url = buildUrl("/api/attachments/upload-image")
|
||||
val inputStream = context.contentResolver.openInputStream(imageUri)
|
||||
?: throw IOException("Cannot open image")
|
||||
val bytes = inputStream.readBytes()
|
||||
inputStream.close()
|
||||
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("type", AttachmentType.SPACE_ICON)
|
||||
.addFormDataPart("spaceId", spaceId)
|
||||
.addFormDataPart("file", "icon.jpg", bytes.toRequestBody("image/*".toMediaType()))
|
||||
.build()
|
||||
|
||||
val builder = Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.addHeader("Cookie", "authToken=$authToken")
|
||||
|
||||
val request = builder.build()
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Icon upload failed: $responseBody")
|
||||
}
|
||||
|
||||
val attachment = gson.fromJson(responseBody, AttachmentUploadResponse::class.java)
|
||||
attachment.fileName
|
||||
}
|
||||
|
||||
suspend fun removeSpaceIcon(spaceId: String) = withContext(Dispatchers.IO) {
|
||||
post("/api/attachments/remove-icon", RemoveIconRequest(AttachmentType.SPACE_ICON, spaceId))
|
||||
}
|
||||
|
||||
suspend fun updatePage(pageId: String, content: String?, title: String?, icon: String? = null, isJson: Boolean = false): Page =
|
||||
withContext(Dispatchers.IO) {
|
||||
val json = post(
|
||||
"/api/pages/update",
|
||||
PageUpdateRequest(
|
||||
pageId = pageId,
|
||||
content = content,
|
||||
title = title,
|
||||
icon = icon,
|
||||
format = if (isJson) "json" else "markdown",
|
||||
operation = "replace"
|
||||
)
|
||||
)
|
||||
val type = object : TypeToken<DocmostResponse<Page>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<Page>>(json, type)
|
||||
response.data ?: throw IOException("Update response missing data")
|
||||
}
|
||||
|
||||
suspend fun getCurrentUser(): User = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/users/me")
|
||||
val type = object : TypeToken<DocmostResponse<UserInfoResponse>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<UserInfoResponse>>(json, type)
|
||||
response.data?.user ?: throw IOException("User not found in response")
|
||||
}
|
||||
|
||||
suspend fun updateUser(name: String?): User = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/users/update", UserUpdateRequest(name = name))
|
||||
val type = object : TypeToken<DocmostResponse<User>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<User>>(json, type)
|
||||
response.data ?: throw IOException("Update user response missing data")
|
||||
}
|
||||
|
||||
suspend fun searchPages(query: String): List<SearchResult> = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/search", SearchRequest(query = query))
|
||||
val type = object : TypeToken<DocmostResponse<SearchResultsResponse>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<SearchResultsResponse>>(json, type)
|
||||
response.data?.items ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun createSpace(name: String, slug: String, description: String? = null): Space =
|
||||
withContext(Dispatchers.IO) {
|
||||
val json = post("/api/spaces/create", CreateSpaceRequest(name = name, slug = slug, description = description))
|
||||
val type = object : TypeToken<DocmostResponse<Space>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<Space>>(json, type)
|
||||
response.data ?: throw IOException("Create space response missing data")
|
||||
}
|
||||
|
||||
suspend fun removeAvatar() = withContext(Dispatchers.IO) {
|
||||
post("/api/attachments/remove-icon", RemoveIconRequest(AttachmentType.AVATAR))
|
||||
}
|
||||
|
||||
suspend fun uploadAvatar(imageUri: Uri, context: Context): String =
|
||||
withContext(Dispatchers.IO) {
|
||||
val url = buildUrl("/api/attachments/upload-image")
|
||||
val inputStream = context.contentResolver.openInputStream(imageUri)
|
||||
?: throw IOException("Cannot open image")
|
||||
val bytes = inputStream.readBytes()
|
||||
inputStream.close()
|
||||
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("type", AttachmentType.AVATAR)
|
||||
.addFormDataPart("file", "avatar.jpg", bytes.toRequestBody("image/*".toMediaType()))
|
||||
.build()
|
||||
|
||||
val builder = Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.addHeader("Cookie", "authToken=$authToken")
|
||||
|
||||
val request = builder.build()
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Avatar upload failed: $responseBody")
|
||||
}
|
||||
|
||||
val attachment = gson.fromJson(responseBody, AttachmentUploadResponse::class.java)
|
||||
attachment.fileName
|
||||
}
|
||||
|
||||
suspend fun getRecentPages(): List<Page> = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/pages/recent", RecentPageRequest())
|
||||
val type = object : TypeToken<DocmostResponse<RecentPagesResponse>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<RecentPagesResponse>>(json, type)
|
||||
response.data?.items ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun createPage(title: String, spaceId: String): Page = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/pages/create", CreatePageRequest(title = title, spaceId = spaceId))
|
||||
val type = object : TypeToken<DocmostResponse<Page>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<Page>>(json, type)
|
||||
response.data ?: throw IOException("Create page response missing data")
|
||||
}
|
||||
|
||||
suspend fun deletePage(pageId: String) = withContext(Dispatchers.IO) {
|
||||
post("/api/pages/delete", DeletePageRequest(pageId = pageId))
|
||||
}
|
||||
|
||||
suspend fun deleteSpace(spaceId: String) = withContext(Dispatchers.IO) {
|
||||
post("/api/spaces/delete", SpaceIdDto(spaceId = spaceId))
|
||||
}
|
||||
|
||||
suspend fun getShares(): List<Share> = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/shares/", mapOf("limit" to 50))
|
||||
val type = object : TypeToken<DocmostResponse<SharesData>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<SharesData>>(json, type)
|
||||
response.data?.items ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun createShare(pageId: String): Share = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/shares/create", CreateShareDto(pageId = pageId))
|
||||
val type = object : TypeToken<DocmostResponse<Share>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<Share>>(json, type)
|
||||
response.data ?: throw IOException("Create share response missing data")
|
||||
}
|
||||
|
||||
suspend fun deleteShare(shareId: String) = withContext(Dispatchers.IO) {
|
||||
post("/api/shares/delete", ShareIdDto(shareId = shareId))
|
||||
}
|
||||
|
||||
suspend fun getComments(pageId: String): List<Comment> = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/comments/", mapOf("pageId" to pageId))
|
||||
val type = object : TypeToken<DocmostResponse<CommentsData>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<CommentsData>>(json, type)
|
||||
response.data?.items ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun createComment(pageId: String, content: String): Comment = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/comments/create", CreateCommentDto(pageId = pageId, content = content))
|
||||
val type = object : TypeToken<DocmostResponse<Comment>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<Comment>>(json, type)
|
||||
response.data ?: throw IOException("Create comment response missing data")
|
||||
}
|
||||
|
||||
suspend fun updateComment(commentId: String, content: String): Comment = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/comments/update", UpdateCommentDto(commentId = commentId, content = content))
|
||||
val type = object : TypeToken<DocmostResponse<Comment>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<Comment>>(json, type)
|
||||
response.data ?: throw IOException("Update comment response missing data")
|
||||
}
|
||||
|
||||
suspend fun deleteComment(commentId: String) = withContext(Dispatchers.IO) {
|
||||
post("/api/comments/delete", CommentIdDto(commentId = commentId))
|
||||
}
|
||||
|
||||
suspend fun duplicatePage(pageId: String, targetSpaceId: String? = null): Page = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/pages/duplicate", DuplicatePageRequest(pageId = pageId, spaceId = targetSpaceId))
|
||||
val type = object : TypeToken<DocmostResponse<Page>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<Page>>(json, type)
|
||||
response.data ?: throw IOException("Duplicate response missing data")
|
||||
}
|
||||
|
||||
suspend fun movePageToSpace(pageId: String, spaceId: String) = withContext(Dispatchers.IO) {
|
||||
post("/api/pages/move-to-space", MovePageToSpaceDto(pageId = pageId, spaceId = spaceId))
|
||||
}
|
||||
|
||||
suspend fun movePageToPage(pageId: String, parentPageId: String?, position: String) = withContext(Dispatchers.IO) {
|
||||
val body: Map<String, Any?> = mapOf(
|
||||
"pageId" to pageId,
|
||||
"parentPageId" to parentPageId,
|
||||
"position" to position
|
||||
)
|
||||
post("/api/pages/move", body)
|
||||
}
|
||||
|
||||
suspend fun getPageHistory(pageId: String): List<PageHistory> = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/pages/history", PageHistoryRequest(pageId = pageId))
|
||||
val type = object : TypeToken<DocmostResponse<PageHistoryResponse>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<PageHistoryResponse>>(json, type)
|
||||
response.data?.items ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun getPageHistoryInfo(pageId: String, historyId: String): PageHistory = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/pages/history/info", mapOf("pageId" to pageId, "historyId" to historyId))
|
||||
val type = object : TypeToken<DocmostResponse<PageHistory>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<PageHistory>>(json, type)
|
||||
response.data ?: throw IOException("Page history not found in response")
|
||||
}
|
||||
|
||||
suspend fun getPageContent(pageId: String): PageContent = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/pages/info", PageInfoRequest(pageId = pageId))
|
||||
val type = object : TypeToken<DocmostResponse<Page>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<Page>>(json, type)
|
||||
val page = response.data ?: throw IOException("Page not found in response")
|
||||
PageContent(source = page.source ?: "")
|
||||
}
|
||||
|
||||
suspend fun getDeletedPages(spaceId: String): List<DeletedPage> = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/pages/trash", DeletedPagesRequest(spaceId = spaceId))
|
||||
val type = object : TypeToken<DocmostResponse<DeletedPagesResponse>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<DeletedPagesResponse>>(json, type)
|
||||
response.data?.items ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun restorePage(pageId: String, spaceId: String? = null) = withContext(Dispatchers.IO) {
|
||||
post("/api/pages/restore", RestorePageRequest(pageId = pageId, spaceId = spaceId))
|
||||
}
|
||||
|
||||
suspend fun uploadAttachment(pageId: String, fileUri: android.net.Uri, context: android.content.Context): Attachment =
|
||||
withContext(Dispatchers.IO) {
|
||||
val url = buildUrl("/api/attachments/upload")
|
||||
val inputStream = context.contentResolver.openInputStream(fileUri)
|
||||
?: throw IOException("Cannot open file")
|
||||
val bytes = inputStream.readBytes()
|
||||
inputStream.close()
|
||||
|
||||
val fileName = getFileNameFromUri(context, fileUri) ?: "file"
|
||||
val mimeType = context.contentResolver.getType(fileUri) ?: "application/octet-stream"
|
||||
|
||||
val requestBody = okhttp3.MultipartBody.Builder()
|
||||
.setType(okhttp3.MultipartBody.FORM)
|
||||
.addFormDataPart("type", AttachmentType.FILE)
|
||||
.addFormDataPart("pageId", pageId)
|
||||
.addFormDataPart("file", fileName, bytes.toRequestBody(mimeType.toMediaTypeOrNull()))
|
||||
.build()
|
||||
|
||||
val builder = okhttp3.Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.addHeader("Cookie", "authToken=$authToken")
|
||||
|
||||
val request = builder.build()
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Attachment upload failed: $responseBody")
|
||||
}
|
||||
|
||||
val attachment = gson.fromJson(responseBody, AttachmentUploadResponse::class.java)
|
||||
|
||||
Attachment(
|
||||
id = attachment.id,
|
||||
fileName = attachment.fileName,
|
||||
fileSize = attachment.fileSize,
|
||||
mimeType = attachment.mimeType,
|
||||
url = getAttachmentUrl(AttachmentType.FILE, attachment.fileName),
|
||||
pageId = pageId
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getPageAttachments(pageId: String): List<Attachment> = withContext(Dispatchers.IO) {
|
||||
val json = post("/api/attachments/page", mapOf("pageId" to pageId))
|
||||
val type = object : TypeToken<DocmostResponse<AttachmentsResponse>>() {}.type
|
||||
val response = gson.fromJson<DocmostResponse<AttachmentsResponse>>(json, type)
|
||||
response.data?.items ?: emptyList()
|
||||
}
|
||||
|
||||
private fun getFileNameFromUri(context: android.content.Context, uri: android.net.Uri): String? {
|
||||
var fileName: String? = null
|
||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
val displayNameIndex = it.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
|
||||
if (displayNameIndex >= 0) {
|
||||
fileName = it.getString(displayNameIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileName ?: uri.lastPathSegment
|
||||
}
|
||||
}
|
||||
366
app/src/main/java/com/docmost/app/api/Models.kt
Normal file
366
app/src/main/java/com/docmost/app/api/Models.kt
Normal file
@@ -0,0 +1,366 @@
|
||||
package com.docmost.app.api
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
data class LoginRequest(
|
||||
val email: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
data class Space(
|
||||
val id: String = "",
|
||||
val name: String = "",
|
||||
val description: String? = null,
|
||||
val logo: String? = null,
|
||||
val slug: String? = null,
|
||||
val creatorId: String? = null,
|
||||
val isPersonal: Boolean = false,
|
||||
val memberCount: Int? = null,
|
||||
val membership: SpaceMembership? = null
|
||||
)
|
||||
|
||||
data class SpaceMembership(
|
||||
val userId: String = "",
|
||||
val role: String = ""
|
||||
)
|
||||
|
||||
data class User(
|
||||
val id: String = "",
|
||||
val email: String = "",
|
||||
val name: String? = null,
|
||||
val avatarUrl: String? = null,
|
||||
val role: String? = null
|
||||
)
|
||||
|
||||
data class UserInfo(
|
||||
val id: String = "",
|
||||
val name: String? = null,
|
||||
val avatarUrl: String? = null
|
||||
)
|
||||
|
||||
data class UserUpdateRequest(
|
||||
val name: String? = null
|
||||
)
|
||||
|
||||
data class UserInfoResponse(
|
||||
val user: User? = null
|
||||
)
|
||||
|
||||
object AttachmentType {
|
||||
const val AVATAR = "avatar"
|
||||
const val WORKSPACE_ICON = "workspace-icon"
|
||||
const val SPACE_ICON = "space-icon"
|
||||
const val PAGE_IMAGE = "page-image"
|
||||
const val FILE = "file"
|
||||
}
|
||||
|
||||
data class PaginationMeta(
|
||||
val limit: Int = 0,
|
||||
val hasNextPage: Boolean = false,
|
||||
val hasPrevPage: Boolean = false,
|
||||
val nextCursor: String? = null,
|
||||
val prevCursor: String? = null
|
||||
)
|
||||
|
||||
data class SpacesData(
|
||||
val items: List<Space> = emptyList(),
|
||||
val meta: PaginationMeta? = null
|
||||
)
|
||||
|
||||
data class PagePermissions(
|
||||
val canEdit: Boolean = true,
|
||||
val hasRestriction: Boolean = false
|
||||
)
|
||||
|
||||
data class Page(
|
||||
val id: String = "",
|
||||
val title: String = "",
|
||||
val content: Any? = null,
|
||||
val spaceId: String? = null,
|
||||
val parentPageId: String? = null,
|
||||
val icon: String? = null,
|
||||
val slugId: String? = null,
|
||||
val hasChildren: Boolean = false,
|
||||
val position: String? = null,
|
||||
val source: String? = null,
|
||||
val creator: UserInfo? = null,
|
||||
val lastUpdatedBy: UserInfo? = null,
|
||||
val createdAt: String = "",
|
||||
val updatedAt: String = "",
|
||||
val labels: List<String>? = null,
|
||||
val permissions: PagePermissions? = null
|
||||
)
|
||||
|
||||
data class PagesData(
|
||||
val items: List<Page> = emptyList(),
|
||||
val meta: PaginationMeta? = null
|
||||
)
|
||||
|
||||
data class PageInfoRequest(
|
||||
val pageId: String,
|
||||
val format: String = "json"
|
||||
)
|
||||
|
||||
data class PageUpdateRequest(
|
||||
val pageId: String,
|
||||
val content: Any? = null,
|
||||
val title: String? = null,
|
||||
val icon: String? = null,
|
||||
val parentPageId: String? = null,
|
||||
val format: String = "markdown",
|
||||
val operation: String = "replace"
|
||||
)
|
||||
|
||||
data class SidebarPagesRequest(
|
||||
val spaceId: String,
|
||||
val pageId: String? = null
|
||||
)
|
||||
|
||||
data class ApiError(
|
||||
val message: String = "Unknown error"
|
||||
)
|
||||
|
||||
data class SpaceUpdateRequest(
|
||||
val spaceId: String,
|
||||
val name: String? = null,
|
||||
val description: String? = null,
|
||||
val slug: String? = null
|
||||
)
|
||||
|
||||
data class RemoveIconRequest(
|
||||
val type: String,
|
||||
val spaceId: String? = null
|
||||
)
|
||||
|
||||
data class AttachmentUploadResponse(
|
||||
val id: String = "",
|
||||
val fileName: String = "",
|
||||
val fileSize: Long = 0,
|
||||
val mimeType: String = ""
|
||||
)
|
||||
|
||||
data class SearchRequest(
|
||||
val query: String,
|
||||
val limit: Int = 25,
|
||||
val offset: Int = 0
|
||||
)
|
||||
|
||||
data class SearchResult(
|
||||
val id: String = "",
|
||||
val title: String = "",
|
||||
val icon: String? = null,
|
||||
val highlight: String? = null,
|
||||
val spaceId: String? = null,
|
||||
val spaceName: String? = null
|
||||
)
|
||||
|
||||
data class SearchResultsResponse(
|
||||
val items: List<SearchResult> = emptyList()
|
||||
)
|
||||
|
||||
data class CreateSpaceRequest(
|
||||
val name: String,
|
||||
val slug: String,
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
data class CreatePageRequest(
|
||||
val title: String,
|
||||
val spaceId: String,
|
||||
val icon: String? = null,
|
||||
val content: String? = null,
|
||||
val format: String? = "markdown",
|
||||
val parentPageId: String? = null
|
||||
)
|
||||
|
||||
data class DeletePageRequest(
|
||||
val pageId: String
|
||||
)
|
||||
|
||||
data class SpaceIdDto(
|
||||
val spaceId: String
|
||||
)
|
||||
|
||||
data class RecentPageRequest(
|
||||
val limit: Int = 50,
|
||||
val offset: Int = 0
|
||||
)
|
||||
|
||||
data class RecentPagesResponse(
|
||||
val items: List<Page> = emptyList()
|
||||
)
|
||||
|
||||
data class DocmostResponse<T>(
|
||||
val data: T? = null,
|
||||
val success: Boolean = false,
|
||||
val status: Int = 0
|
||||
)
|
||||
|
||||
data class DuplicatePageRequest(
|
||||
val pageId: String,
|
||||
val spaceId: String? = null
|
||||
)
|
||||
|
||||
data class MovePageToSpaceDto(
|
||||
val pageId: String,
|
||||
val spaceId: String
|
||||
)
|
||||
|
||||
data class Share(
|
||||
val id: String = "",
|
||||
val pageId: String = "",
|
||||
val key: String = "",
|
||||
val spaceId: String = "",
|
||||
val includeSubPages: Boolean = false,
|
||||
val searchIndexing: Boolean = false,
|
||||
val createdAt: String = "",
|
||||
val page: SharePageInfo? = null
|
||||
)
|
||||
|
||||
data class SharePageInfo(
|
||||
val id: String = "",
|
||||
val title: String = "",
|
||||
val slugId: String = "",
|
||||
val icon: String? = null
|
||||
)
|
||||
|
||||
data class SharesData(
|
||||
val items: List<Share> = emptyList()
|
||||
)
|
||||
|
||||
data class CreateShareDto(
|
||||
val pageId: String,
|
||||
val includeSubPages: Boolean = false,
|
||||
val searchIndexing: Boolean = false
|
||||
)
|
||||
|
||||
data class ShareIdDto(
|
||||
val shareId: String
|
||||
)
|
||||
|
||||
data class Comment(
|
||||
val id: String = "",
|
||||
val pageId: String = "",
|
||||
val content: com.google.gson.JsonObject? = null,
|
||||
val parentCommentId: String? = null,
|
||||
val creatorId: String = "",
|
||||
val createdAt: String = "",
|
||||
val updatedAt: String = "",
|
||||
val creator: CommentCreator? = null
|
||||
)
|
||||
|
||||
data class CommentCreator(
|
||||
val id: String = "",
|
||||
val name: String? = null,
|
||||
val avatarUrl: String? = null
|
||||
)
|
||||
|
||||
data class CommentsData(
|
||||
val items: List<Comment> = emptyList()
|
||||
)
|
||||
|
||||
data class CreateCommentDto(
|
||||
val pageId: String,
|
||||
val content: String
|
||||
)
|
||||
|
||||
data class UpdateCommentDto(
|
||||
val commentId: String,
|
||||
val content: String
|
||||
)
|
||||
|
||||
data class CommentIdDto(
|
||||
val commentId: String
|
||||
)
|
||||
|
||||
data class PageHistory(
|
||||
val id: String = "",
|
||||
val pageId: String = "",
|
||||
val version: Int = 0,
|
||||
val createdAt: String = "",
|
||||
val lastUpdatedBy: UserInfo? = null,
|
||||
val contributorIds: List<String> = emptyList(),
|
||||
val content: Any? = null
|
||||
)
|
||||
|
||||
data class PageHistoryRequest(
|
||||
val pageId: String,
|
||||
val limit: Int = 20,
|
||||
val cursor: String? = null
|
||||
)
|
||||
|
||||
data class PageHistoryResponse(
|
||||
val items: List<PageHistory> = emptyList(),
|
||||
val hasNextPage: Boolean = false,
|
||||
val nextCursor: String? = null
|
||||
)
|
||||
|
||||
data class FavoriteCreateDto(
|
||||
val type: String,
|
||||
val pageId: String
|
||||
)
|
||||
|
||||
data class FavoriteRemoveDto(
|
||||
val type: String,
|
||||
val pageId: String
|
||||
)
|
||||
|
||||
data class FavoriteIdsDto(
|
||||
val type: String
|
||||
)
|
||||
|
||||
data class FavoriteIdsResponse(
|
||||
val items: List<String> = emptyList()
|
||||
)
|
||||
|
||||
data class PageContent(
|
||||
val source: String
|
||||
)
|
||||
|
||||
data class DeletedPage(
|
||||
val id: String = "",
|
||||
val title: String = "",
|
||||
val icon: String? = null,
|
||||
val spaceId: String? = null,
|
||||
val deletedAt: String = "",
|
||||
val deletedBy: UserInfo? = null
|
||||
)
|
||||
|
||||
data class DeletedPagesResponse(
|
||||
val items: List<DeletedPage> = emptyList()
|
||||
)
|
||||
|
||||
data class DeletedPagesRequest(
|
||||
val spaceId: String,
|
||||
val limit: Int = 50,
|
||||
val cursor: String? = null
|
||||
)
|
||||
|
||||
data class RestorePageRequest(
|
||||
val pageId: String,
|
||||
val spaceId: String? = null
|
||||
)
|
||||
|
||||
data class Attachment(
|
||||
val id: String = "",
|
||||
val fileName: String = "",
|
||||
val fileSize: Long = 0,
|
||||
val mimeType: String = "",
|
||||
val url: String = "",
|
||||
val pageId: String = "",
|
||||
val uploadedBy: UserInfo? = null,
|
||||
val createdAt: String = ""
|
||||
)
|
||||
|
||||
data class AttachmentUploadRequest(
|
||||
val pageId: String,
|
||||
val type: String = "attachment"
|
||||
)
|
||||
|
||||
data class AttachmentsResponse(
|
||||
val items: List<Attachment> = emptyList()
|
||||
)
|
||||
|
||||
const val MAX_ATTACHMENT_SIZE_MB = 30
|
||||
|
||||
class OfflineNotAvailableException(message: String) : IOException(message)
|
||||
187
app/src/main/java/com/docmost/app/data/PageCacheManager.kt
Normal file
187
app/src/main/java/com/docmost/app/data/PageCacheManager.kt
Normal file
@@ -0,0 +1,187 @@
|
||||
package com.docmost.app.data
|
||||
|
||||
import android.content.Context
|
||||
import com.docmost.app.api.Page
|
||||
import com.docmost.app.BuildConfig
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
data class CachedPageMeta(
|
||||
val pageId: String,
|
||||
val title: String,
|
||||
val icon: String? = null,
|
||||
val spaceId: String? = null,
|
||||
val isPending: Boolean = false,
|
||||
val updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
class PageCacheManager(private val context: Context) {
|
||||
|
||||
private val gson = Gson()
|
||||
private var currentUserId: String = "default"
|
||||
|
||||
private val cacheDir: File
|
||||
get() {
|
||||
val userDir = File(context.cacheDir, "pages_cache")
|
||||
val userSpecificDir = File(userDir, currentUserId)
|
||||
return userSpecificDir.also { it.mkdirs() }
|
||||
}
|
||||
private val indexFile: File
|
||||
get() = File(cacheDir, "index.json")
|
||||
private val draftsDir: File
|
||||
get() {
|
||||
val dir = File(cacheDir, "drafts")
|
||||
return dir.also { it.mkdirs() }
|
||||
}
|
||||
|
||||
private val spacePagesDir: File
|
||||
get() {
|
||||
val dir = File(cacheDir, "space_pages")
|
||||
return dir.also { it.mkdirs() }
|
||||
}
|
||||
|
||||
fun setCurrentUser(email: String, serverUrl: String) {
|
||||
val uniqueId = "${email.lowercase()}@${serverUrl.hash()}"
|
||||
currentUserId = uniqueId
|
||||
}
|
||||
|
||||
private fun String.hash(): String {
|
||||
return MessageDigest.getInstance("SHA-256")
|
||||
.digest(this.toByteArray())
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
.take(16)
|
||||
}
|
||||
|
||||
fun cachePage(page: Page) {
|
||||
try {
|
||||
val pageFile = File(cacheDir, "${page.id}.json")
|
||||
pageFile.writeText(gson.toJson(page))
|
||||
|
||||
val meta = CachedPageMeta(
|
||||
pageId = page.id,
|
||||
title = page.title,
|
||||
icon = page.icon,
|
||||
spaceId = page.spaceId,
|
||||
isPending = false,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
val index = getIndex().toMutableList()
|
||||
index.removeAll { it.pageId == page.id }
|
||||
index.add(meta)
|
||||
indexFile.writeText(gson.toJson(index))
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) android.util.Log.e("PageCache", "Cache error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDraft(pageId: String, content: String, title: String, icon: String?) {
|
||||
try {
|
||||
val draftFile = File(draftsDir, "$pageId.json")
|
||||
val draft = mapOf(
|
||||
"pageId" to pageId,
|
||||
"content" to content,
|
||||
"title" to title,
|
||||
"icon" to icon,
|
||||
"updatedAt" to System.currentTimeMillis()
|
||||
)
|
||||
draftFile.writeText(gson.toJson(draft))
|
||||
if (BuildConfig.DEBUG) android.util.Log.d("PageCache", "Draft saved for page: $pageId")
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) android.util.Log.e("PageCache", "Draft save error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getDraft(pageId: String): Map<String, Any?>? {
|
||||
return try {
|
||||
val draftFile = File(draftsDir, "$pageId.json")
|
||||
if (!draftFile.exists()) return null
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
gson.fromJson(draftFile.readText(), Map::class.java) as? Map<String, Any?>
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDraft(pageId: String) {
|
||||
try {
|
||||
File(draftsDir, "$pageId.json").delete()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
fun getCachedPage(pageId: String): Page? {
|
||||
return try {
|
||||
val pageFile = File(cacheDir, "$pageId.json")
|
||||
if (!pageFile.exists()) return null
|
||||
gson.fromJson(pageFile.readText(), Page::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedPages(): List<CachedPageMeta> {
|
||||
return try {
|
||||
if (!indexFile.exists()) return emptyList()
|
||||
val type = object : TypeToken<List<CachedPageMeta>>() {}.type
|
||||
gson.fromJson(indexFile.readText(), type) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromCache(pageId: String) {
|
||||
try {
|
||||
File(cacheDir, "$pageId.json").delete()
|
||||
deleteDraft(pageId)
|
||||
val index = getIndex().toMutableList()
|
||||
index.removeAll { it.pageId == pageId }
|
||||
indexFile.writeText(gson.toJson(index))
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
fun cachePagesForSpace(spaceId: String, pages: List<Page>) {
|
||||
try {
|
||||
val file = File(spacePagesDir, "$spaceId.json")
|
||||
file.writeText(gson.toJson(pages))
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) android.util.Log.e("PageCache", "Cache space pages error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedPagesForSpace(spaceId: String): List<Page>? {
|
||||
return try {
|
||||
val file = File(spacePagesDir, "$spaceId.json")
|
||||
if (!file.exists()) return null
|
||||
val type = object : TypeToken<List<Page>>() {}.type
|
||||
gson.fromJson(file.readText(), type)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAll() {
|
||||
try {
|
||||
cacheDir.deleteRecursively()
|
||||
cacheDir.mkdirs()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
fun clearUserCache() {
|
||||
try {
|
||||
cacheDir.deleteRecursively()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
fun getCachedPageCount(): Int = getIndex().size
|
||||
|
||||
private fun getIndex(): List<CachedPageMeta> {
|
||||
return try {
|
||||
if (!indexFile.exists()) return emptyList()
|
||||
val type = object : TypeToken<List<CachedPageMeta>>() {}.type
|
||||
gson.fromJson(indexFile.readText(), type) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
139
app/src/main/java/com/docmost/app/data/SessionManager.kt
Normal file
139
app/src/main/java/com/docmost/app/data/SessionManager.kt
Normal file
@@ -0,0 +1,139 @@
|
||||
package com.docmost.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import com.google.gson.Gson
|
||||
|
||||
|
||||
data class Credentials(
|
||||
val url: String,
|
||||
val email: String,
|
||||
val password: String,
|
||||
val authToken: String = "",
|
||||
val avatarUrl: String? = null
|
||||
)
|
||||
|
||||
class SessionManager(private val context: Context) {
|
||||
|
||||
private val gson = Gson()
|
||||
|
||||
private val prefs: SharedPreferences by lazy {
|
||||
createEncryptedPrefs(context, PREFS_NAME)
|
||||
}
|
||||
|
||||
private val savedAccountsPrefs: SharedPreferences by lazy {
|
||||
createEncryptedPrefs(context, SAVED_ACCOUNTS_PREFS)
|
||||
}
|
||||
|
||||
private fun createEncryptedPrefs(context: Context, prefsName: String): SharedPreferences {
|
||||
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||
return try {
|
||||
EncryptedSharedPreferences.create(
|
||||
prefsName,
|
||||
masterKeyAlias,
|
||||
context,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
context.getSharedPreferences(prefsName, Context.MODE_PRIVATE).edit().clear().apply()
|
||||
EncryptedSharedPreferences.create(
|
||||
prefsName,
|
||||
masterKeyAlias,
|
||||
context,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveCredentials(credentials: Credentials) {
|
||||
try {
|
||||
val json = gson.toJson(credentials)
|
||||
prefs.edit().putString(KEY_CREDENTIALS, json).apply()
|
||||
} catch (e: Exception) {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun getCredentials(): Credentials? {
|
||||
val json = try {
|
||||
prefs.getString(KEY_CREDENTIALS, null)
|
||||
} catch (e: Exception) {
|
||||
clear()
|
||||
null
|
||||
} ?: return null
|
||||
return try {
|
||||
gson.fromJson(json, Credentials::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun saveAuthToken(token: String) {
|
||||
val current = getCredentials() ?: return
|
||||
saveCredentials(current.copy(authToken = token))
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
try {
|
||||
prefs.edit().clear().apply()
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
fun hasSession(): Boolean {
|
||||
val creds = getCredentials() ?: return false
|
||||
return creds.url.isNotBlank() && creds.email.isNotBlank() && creds.authToken.isNotBlank()
|
||||
}
|
||||
|
||||
fun saveAccount(credentials: Credentials) {
|
||||
try {
|
||||
val accounts = getSavedAccounts().toMutableList()
|
||||
accounts.removeAll { it.email == credentials.email && it.url == credentials.url }
|
||||
accounts.add(credentials)
|
||||
val json = gson.toJson(accounts)
|
||||
savedAccountsPrefs.edit().putString(KEY_SAVED_ACCOUNTS, json).apply()
|
||||
} catch (e: Exception) {
|
||||
context.getSharedPreferences(SAVED_ACCOUNTS_PREFS, Context.MODE_PRIVATE).edit().clear().commit()
|
||||
val json = gson.toJson(listOf(credentials))
|
||||
savedAccountsPrefs.edit().putString(KEY_SAVED_ACCOUNTS, json).commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSavedAccounts(): List<Credentials> {
|
||||
val json = try {
|
||||
savedAccountsPrefs.getString(KEY_SAVED_ACCOUNTS, null)
|
||||
} catch (e: Exception) {
|
||||
context.getSharedPreferences(SAVED_ACCOUNTS_PREFS, Context.MODE_PRIVATE).edit().clear().commit()
|
||||
null
|
||||
} ?: return emptyList()
|
||||
return try {
|
||||
val type = com.google.gson.reflect.TypeToken.getParameterized(List::class.java, Credentials::class.java).type
|
||||
gson.fromJson(json, type)
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAccount(email: String, url: String) {
|
||||
try {
|
||||
val accounts = getSavedAccounts().toMutableList()
|
||||
accounts.removeAll { it.email == email && it.url == url }
|
||||
val json = gson.toJson(accounts)
|
||||
savedAccountsPrefs.edit().putString(KEY_SAVED_ACCOUNTS, json).apply()
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
fun hasAccount(email: String, url: String): Boolean {
|
||||
return getSavedAccounts().any { it.email == email && it.url == url }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "docmost_session"
|
||||
private const val KEY_CREDENTIALS = "credentials"
|
||||
private const val SAVED_ACCOUNTS_PREFS = "docmost_saved_accounts"
|
||||
private const val KEY_SAVED_ACCOUNTS = "saved_accounts"
|
||||
}
|
||||
}
|
||||
135
app/src/main/java/com/docmost/app/data/SpaceCacheManager.kt
Normal file
135
app/src/main/java/com/docmost/app/data/SpaceCacheManager.kt
Normal file
@@ -0,0 +1,135 @@
|
||||
package com.docmost.app.data
|
||||
|
||||
import android.content.Context
|
||||
import com.docmost.app.api.Space
|
||||
import com.docmost.app.BuildConfig
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
data class CachedSpaceMeta(
|
||||
val spaceId: String,
|
||||
val name: String,
|
||||
val slug: String? = null,
|
||||
val isPending: Boolean = false,
|
||||
val updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
class SpaceCacheManager(private val context: Context) {
|
||||
|
||||
private val gson = Gson()
|
||||
private var currentUserId: String = "default"
|
||||
|
||||
private val cacheDir: File
|
||||
get() {
|
||||
val userDir = File(context.cacheDir, "spaces_cache")
|
||||
val userSpecificDir = File(userDir, currentUserId)
|
||||
return userSpecificDir.also { it.mkdirs() }
|
||||
}
|
||||
|
||||
private val indexFile: File
|
||||
get() = File(cacheDir, "index.json")
|
||||
|
||||
private fun String.hash(): String {
|
||||
return MessageDigest.getInstance("SHA-256")
|
||||
.digest(this.toByteArray())
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
.take(16)
|
||||
}
|
||||
|
||||
fun setCurrentUser(email: String, serverUrl: String) {
|
||||
val uniqueId = "${email.lowercase()}@${serverUrl.hash()}"
|
||||
currentUserId = uniqueId
|
||||
}
|
||||
|
||||
fun cacheSpaces(spaces: List<Space>) {
|
||||
try {
|
||||
val index = mutableListOf<CachedSpaceMeta>()
|
||||
for (space in spaces) {
|
||||
val spaceFile = File(cacheDir, "${space.id}.json")
|
||||
spaceFile.writeText(gson.toJson(space))
|
||||
|
||||
index.add(CachedSpaceMeta(
|
||||
spaceId = space.id,
|
||||
name = space.name,
|
||||
slug = space.id,
|
||||
isPending = false,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
))
|
||||
}
|
||||
indexFile.writeText(gson.toJson(index))
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) android.util.Log.e("SpaceCache", "Cache spaces error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun cacheSpace(space: Space) {
|
||||
try {
|
||||
val spaceFile = File(cacheDir, "${space.id}.json")
|
||||
spaceFile.writeText(gson.toJson(space))
|
||||
|
||||
val index = getIndex().toMutableList()
|
||||
index.removeAll { it.spaceId == space.id }
|
||||
index.add(CachedSpaceMeta(
|
||||
spaceId = space.id,
|
||||
name = space.name,
|
||||
slug = space.id,
|
||||
isPending = false,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
))
|
||||
indexFile.writeText(gson.toJson(index))
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) android.util.Log.e("SpaceCache", "Cache space error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedSpaces(): List<Space> {
|
||||
return try {
|
||||
val index = getIndex()
|
||||
index.mapNotNull { meta ->
|
||||
val spaceFile = File(cacheDir, "${meta.spaceId}.json")
|
||||
if (!spaceFile.exists()) return@mapNotNull null
|
||||
gson.fromJson(spaceFile.readText(), Space::class.java)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedSpace(spaceId: String): Space? {
|
||||
return try {
|
||||
val spaceFile = File(cacheDir, "$spaceId.json")
|
||||
if (!spaceFile.exists()) return null
|
||||
gson.fromJson(spaceFile.readText(), Space::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun removeCachedSpace(spaceId: String) {
|
||||
try {
|
||||
File(cacheDir, "$spaceId.json").delete()
|
||||
val index = getIndex().toMutableList()
|
||||
index.removeAll { it.spaceId == spaceId }
|
||||
indexFile.writeText(gson.toJson(index))
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
fun getIndex(): List<CachedSpaceMeta> {
|
||||
return try {
|
||||
if (!indexFile.exists()) return emptyList()
|
||||
val type = object : TypeToken<List<CachedSpaceMeta>>() {}.type
|
||||
gson.fromJson(indexFile.readText(), type) ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAll() {
|
||||
try {
|
||||
cacheDir.deleteRecursively()
|
||||
cacheDir.mkdirs()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
504
app/src/main/java/com/docmost/app/data/SyncManager.kt
Normal file
504
app/src/main/java/com/docmost/app/data/SyncManager.kt
Normal file
@@ -0,0 +1,504 @@
|
||||
package com.docmost.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.docmost.app.BuildConfig
|
||||
import androidx.work.*
|
||||
import com.docmost.app.api.DocmostApi
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class SyncResult(
|
||||
val success: Boolean,
|
||||
val message: String,
|
||||
val conflictsResolved: Int = 0,
|
||||
val operationsProcessed: Int = 0
|
||||
)
|
||||
|
||||
class SyncManager(
|
||||
private val context: Context,
|
||||
private val api: DocmostApi,
|
||||
private val pageCacheManager: PageCacheManager,
|
||||
private val spaceCacheManager: SpaceCacheManager
|
||||
) {
|
||||
private val db = SyncQueueDatabase.getDatabase(context)
|
||||
private val dao = db.syncQueueDao()
|
||||
private val gson = Gson()
|
||||
|
||||
private val _syncStatus = MutableStateFlow(SyncStatus.IDLE)
|
||||
val syncStatus: Flow<SyncStatus> = _syncStatus.asStateFlow()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
private val syncMutex = Mutex()
|
||||
|
||||
init {
|
||||
setupWorkManager()
|
||||
}
|
||||
|
||||
private fun setupWorkManager() {
|
||||
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
"sync_queue_worker",
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
syncRequest
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun enqueueOperation(operation: PendingOperation) {
|
||||
dao.insertOperation(operation)
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Enqueued operation: ${operation.type} for ${operation.entityId}")
|
||||
|
||||
if (operation.type == OperationType.CREATE_PAGE || operation.type == OperationType.CREATE_SPACE) {
|
||||
scheduleImmediateSync()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun enqueueCreateSpace(name: String, slug: String, description: String?, tempId: String) {
|
||||
val payload = gson.toJson(mapOf(
|
||||
"name" to name,
|
||||
"slug" to slug,
|
||||
"description" to description,
|
||||
"tempId" to tempId
|
||||
))
|
||||
val operation = PendingOperation(
|
||||
type = OperationType.CREATE_SPACE,
|
||||
entityType = "space",
|
||||
entityId = null,
|
||||
payload = payload,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
tempId = tempId
|
||||
)
|
||||
enqueueOperation(operation)
|
||||
}
|
||||
|
||||
suspend fun enqueueUpdateSpace(spaceId: String, name: String?, description: String?) {
|
||||
val payload = gson.toJson(mapOf(
|
||||
"spaceId" to spaceId,
|
||||
"name" to name,
|
||||
"description" to description
|
||||
))
|
||||
val operation = PendingOperation(
|
||||
type = OperationType.UPDATE_SPACE,
|
||||
entityType = "space",
|
||||
entityId = spaceId,
|
||||
payload = payload,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
enqueueOperation(operation)
|
||||
}
|
||||
|
||||
suspend fun enqueueCreatePage(title: String, spaceId: String, tempId: String, content: String? = null) {
|
||||
val payload = gson.toJson(mapOf(
|
||||
"title" to title,
|
||||
"spaceId" to spaceId,
|
||||
"content" to content,
|
||||
"tempId" to tempId
|
||||
))
|
||||
val operation = PendingOperation(
|
||||
type = OperationType.CREATE_PAGE,
|
||||
entityType = "page",
|
||||
entityId = null,
|
||||
payload = payload,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
tempId = tempId
|
||||
)
|
||||
enqueueOperation(operation)
|
||||
}
|
||||
|
||||
suspend fun enqueueUpdatePage(pageId: String, content: String, title: String, icon: String?) {
|
||||
// Reemplazar operaciones UPDATE_PAGE previas para la misma página
|
||||
// para que solo se sincronice el contenido más reciente
|
||||
val existing = dao.getOperationByEntity(pageId, OperationType.UPDATE_PAGE)
|
||||
if (existing != null) {
|
||||
dao.deleteOperation(existing)
|
||||
}
|
||||
|
||||
val payload = gson.toJson(mapOf(
|
||||
"pageId" to pageId,
|
||||
"content" to content,
|
||||
"title" to title,
|
||||
"icon" to icon,
|
||||
"updatedAt" to System.currentTimeMillis()
|
||||
))
|
||||
val operation = PendingOperation(
|
||||
type = OperationType.UPDATE_PAGE,
|
||||
entityType = "page",
|
||||
entityId = pageId,
|
||||
payload = payload,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
enqueueOperation(operation)
|
||||
}
|
||||
|
||||
suspend fun enqueueDeletePage(pageId: String) {
|
||||
val payload = gson.toJson(mapOf("pageId" to pageId))
|
||||
val operation = PendingOperation(
|
||||
type = OperationType.DELETE_PAGE,
|
||||
entityType = "page",
|
||||
entityId = pageId,
|
||||
payload = payload,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
enqueueOperation(operation)
|
||||
}
|
||||
|
||||
suspend fun enqueueDeleteSpace(spaceId: String) {
|
||||
val payload = gson.toJson(mapOf("spaceId" to spaceId))
|
||||
val operation = PendingOperation(
|
||||
type = OperationType.DELETE_SPACE,
|
||||
entityType = "space",
|
||||
entityId = spaceId,
|
||||
payload = payload,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
enqueueOperation(operation)
|
||||
}
|
||||
|
||||
suspend fun enqueueUploadAttachment(pageId: String, localPath: String, fileName: String, mimeType: String) {
|
||||
val payload = gson.toJson(mapOf(
|
||||
"pageId" to pageId,
|
||||
"localPath" to localPath,
|
||||
"fileName" to fileName,
|
||||
"mimeType" to mimeType
|
||||
))
|
||||
val operation = PendingOperation(
|
||||
type = OperationType.UPLOAD_ATTACHMENT,
|
||||
entityType = "attachment",
|
||||
entityId = pageId,
|
||||
payload = payload,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
enqueueOperation(operation)
|
||||
}
|
||||
|
||||
suspend fun processQueue(): SyncResult {
|
||||
syncMutex.withLock {
|
||||
if (_syncStatus.value == SyncStatus.SYNCING) {
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Already syncing, skipping concurrent call")
|
||||
return SyncResult(success = true, message = "Ya sincronizando", operationsProcessed = 0)
|
||||
}
|
||||
_syncStatus.value = SyncStatus.SYNCING
|
||||
var conflictsResolved = 0
|
||||
var operationsProcessed = 0
|
||||
|
||||
try {
|
||||
val operations = dao.getAllOperationsList()
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Processing ${operations.size} pending operations")
|
||||
|
||||
for (operation in operations) {
|
||||
try {
|
||||
when (operation.type) {
|
||||
OperationType.CREATE_SPACE -> handleCreateSpace(operation)
|
||||
OperationType.UPDATE_SPACE -> handleUpdateSpace(operation)
|
||||
OperationType.DELETE_SPACE -> handleDeleteSpace(operation)
|
||||
OperationType.CREATE_PAGE -> handleCreatePage(operation)
|
||||
OperationType.UPDATE_PAGE -> handleUpdatePage(operation)
|
||||
OperationType.DELETE_PAGE -> handleDeletePage(operation)
|
||||
OperationType.UPLOAD_ATTACHMENT -> handleUploadAttachment(operation)
|
||||
else -> if (BuildConfig.DEBUG) Log.w("SyncManager", "Unknown operation type: ${operation.type}")
|
||||
}
|
||||
|
||||
dao.deleteOperation(operation)
|
||||
operationsProcessed++
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Completed operation: ${operation.type}")
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) Log.e("SyncManager", "Error processing operation ${operation.type}: ${e.message}")
|
||||
|
||||
if (e.message?.contains("409") == true || e.message?.contains("conflict") == true) {
|
||||
conflictsResolved++
|
||||
dao.deleteOperation(operation)
|
||||
} else if (operation.retryCount < 3) {
|
||||
val updated = operation.copy(retryCount = operation.retryCount + 1)
|
||||
dao.updateOperation(updated)
|
||||
} else {
|
||||
dao.deleteOperation(operation)
|
||||
if (BuildConfig.DEBUG) Log.e("SyncManager", "Operation failed after 3 retries: ${operation.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result = SyncResult(
|
||||
success = true,
|
||||
message = "Sincronización completada: $operationsProcessed operaciones",
|
||||
conflictsResolved = conflictsResolved,
|
||||
operationsProcessed = operationsProcessed
|
||||
)
|
||||
_syncStatus.value = SyncStatus.IDLE
|
||||
return result
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) Log.e("SyncManager", "Sync failed: ${e.message}")
|
||||
_syncStatus.value = SyncStatus.IDLE
|
||||
return SyncResult(
|
||||
success = false,
|
||||
message = "Error de sincronización: ${e.message}",
|
||||
conflictsResolved = conflictsResolved
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private suspend fun handleCreateSpace(operation: PendingOperation) {
|
||||
val payload = gson.fromJson(operation.payload, Map::class.java) as Map<String, Any?>
|
||||
val name = payload["name"] as String
|
||||
var slug = payload["slug"] as String
|
||||
val description = payload["description"] as? String
|
||||
val tempId = payload["tempId"] as? String
|
||||
|
||||
var space: com.docmost.app.api.Space? = null
|
||||
var lastError: Exception? = null
|
||||
for (attempt in 0 until 3) {
|
||||
try {
|
||||
space = api.createSpace(name, slug, description)
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Space created on server: ${space.id} (was temp: $tempId)")
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
lastError = e
|
||||
if (e.message?.contains("slug exists", ignoreCase = true) == true) {
|
||||
slug = "${slug}_${System.currentTimeMillis()}"
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Slug collision, retrying with: $slug")
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
if (space == null) throw lastError ?: Exception("Failed to create space")
|
||||
|
||||
// Replace tempId with real ID in queued operations that reference it
|
||||
if (tempId != null) {
|
||||
val pendingOps = dao.getAllOperationsList()
|
||||
for (op in pendingOps) {
|
||||
try {
|
||||
val opPayload = gson.fromJson(op.payload, Map::class.java) as Map<String, Any?>
|
||||
val updatedPayload: Map<String, Any?>? = when {
|
||||
op.type == OperationType.UPDATE_SPACE && opPayload["spaceId"] == tempId -> {
|
||||
opPayload + ("spaceId" to space.id)
|
||||
}
|
||||
op.type == OperationType.CREATE_PAGE && opPayload["spaceId"] == tempId -> {
|
||||
opPayload + ("spaceId" to space.id)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
if (updatedPayload != null) {
|
||||
val updatedOp = op.copy(payload = gson.toJson(updatedPayload))
|
||||
dao.updateOperation(updatedOp)
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Updated ${op.type} payload: temp $tempId → ${space.id}")
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
// Update space cache: replace temp entry with real server data
|
||||
spaceCacheManager.cacheSpaces(listOf(space))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private suspend fun handleUpdateSpace(operation: PendingOperation) {
|
||||
val payload = gson.fromJson(operation.payload, Map::class.java) as Map<String, Any?>
|
||||
val spaceId = payload["spaceId"] as String
|
||||
val name = payload["name"] as? String
|
||||
val description = payload["description"] as? String
|
||||
|
||||
api.updateSpace(spaceId, name, description)
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Space updated on server: $spaceId")
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private suspend fun handleDeleteSpace(operation: PendingOperation) {
|
||||
val payload = gson.fromJson(operation.payload, Map::class.java) as Map<String, Any?>
|
||||
val spaceId = payload["spaceId"] as String
|
||||
api.deleteSpace(spaceId)
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Space deleted on server: $spaceId")
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private suspend fun handleCreatePage(operation: PendingOperation) {
|
||||
val payload = gson.fromJson(operation.payload, Map::class.java) as Map<String, Any?>
|
||||
val title = payload["title"] as String
|
||||
val spaceId = payload["spaceId"] as String
|
||||
val content = payload["content"] as? String
|
||||
val tempId = payload["tempId"] as? String
|
||||
|
||||
val page = api.createPage(title, spaceId)
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Page created on server: ${page.id} (was temp: $tempId)")
|
||||
|
||||
if (tempId != null) {
|
||||
val pendingOps = dao.getAllOperationsList()
|
||||
for (op in pendingOps) {
|
||||
try {
|
||||
val opPayload = gson.fromJson(op.payload, Map::class.java) as Map<String, Any?>
|
||||
if (op.type == OperationType.DELETE_PAGE && opPayload["pageId"] == tempId) {
|
||||
val updatedPayload = opPayload + ("pageId" to page.id)
|
||||
val updatedOp = op.copy(payload = gson.toJson(updatedPayload))
|
||||
dao.updateOperation(updatedOp)
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Updated DELETE_PAGE payload: temp $tempId → ${page.id}")
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
|
||||
pageCacheManager.cachePage(page)
|
||||
}
|
||||
|
||||
if (content != null) {
|
||||
try {
|
||||
api.updatePage(page.id, content, title, null)
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) Log.e("SyncManager", "Failed to set page content: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private suspend fun handleUpdatePage(operation: PendingOperation) {
|
||||
val payload = gson.fromJson(operation.payload, Map::class.java) as Map<String, Any?>
|
||||
val pageId = payload["pageId"] as String
|
||||
val content = payload["content"] as String
|
||||
val title = payload["title"] as String
|
||||
val icon = payload["icon"] as? String
|
||||
|
||||
try {
|
||||
val remotePage = api.getPageInfo(pageId)
|
||||
|
||||
api.updatePage(pageId, content, title, icon)
|
||||
pageCacheManager.cachePage(remotePage.copy(
|
||||
title = title,
|
||||
icon = icon,
|
||||
content = content
|
||||
))
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Page updated on server: $pageId")
|
||||
} catch (e: Exception) {
|
||||
if (e.message?.contains("404") == true) {
|
||||
api.updatePage(pageId, content, title, icon)
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Page updated on server (page not found remotely): $pageId")
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private suspend fun handleDeletePage(operation: PendingOperation) {
|
||||
val payload = gson.fromJson(operation.payload, Map::class.java) as Map<String, Any?>
|
||||
val pageId = payload["pageId"] as String
|
||||
api.deletePage(pageId)
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Page deleted on server: $pageId")
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private suspend fun handleUploadAttachment(operation: PendingOperation) {
|
||||
val payload = gson.fromJson(operation.payload, Map::class.java) as Map<String, Any?>
|
||||
val pageId = payload["pageId"] as String
|
||||
val localPath = payload["localPath"] as String
|
||||
val fileName = payload["fileName"] as String
|
||||
val mimeType = payload["mimeType"] as String
|
||||
|
||||
val fileUri = android.net.Uri.parse(localPath)
|
||||
api.uploadAttachment(pageId, fileUri, context)
|
||||
if (BuildConfig.DEBUG) Log.d("SyncManager", "Attachment uploaded for page: $pageId")
|
||||
}
|
||||
|
||||
fun observePendingCount(): Flow<Int> {
|
||||
return dao.observePendingCount()
|
||||
}
|
||||
|
||||
suspend fun getPendingCount(): Int {
|
||||
return dao.getPendingCount()
|
||||
}
|
||||
|
||||
suspend fun clearAll() {
|
||||
dao.deleteAll()
|
||||
}
|
||||
|
||||
private fun scheduleImmediateSync() {
|
||||
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||
.setConstraints(
|
||||
Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueue(syncRequest)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: SyncManager? = null
|
||||
|
||||
fun getInstance(
|
||||
context: Context,
|
||||
api: DocmostApi,
|
||||
pageCacheManager: PageCacheManager,
|
||||
spaceCacheManager: SpaceCacheManager
|
||||
): SyncManager {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = SyncManager(context.applicationContext, api, pageCacheManager, spaceCacheManager)
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SyncStatus {
|
||||
IDLE,
|
||||
SYNCING,
|
||||
ERROR
|
||||
}
|
||||
|
||||
class SyncWorker(
|
||||
private val context: Context,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return try {
|
||||
val syncManager = SyncManager.getInstance(
|
||||
context,
|
||||
createApi(),
|
||||
PageCacheManager(context),
|
||||
SpaceCacheManager(context)
|
||||
)
|
||||
val result = syncManager.processQueue()
|
||||
|
||||
if (result.success) {
|
||||
Result.success()
|
||||
} else {
|
||||
Result.retry()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) Log.e("SyncWorker", "Sync failed: ${e.message}")
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createApi(): DocmostApi {
|
||||
val sessionManager = com.docmost.app.data.SessionManager(context)
|
||||
val creds = sessionManager.getCredentials()
|
||||
val api = DocmostApi(creds?.url ?: "")
|
||||
api.setAuthToken(creds?.authToken)
|
||||
return api
|
||||
}
|
||||
}
|
||||
56
app/src/main/java/com/docmost/app/data/SyncQueue.kt
Normal file
56
app/src/main/java/com/docmost/app/data/SyncQueue.kt
Normal file
@@ -0,0 +1,56 @@
|
||||
package com.docmost.app.data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
enum class OperationType {
|
||||
CREATE_SPACE,
|
||||
UPDATE_SPACE,
|
||||
DELETE_SPACE,
|
||||
CREATE_PAGE,
|
||||
UPDATE_PAGE,
|
||||
DELETE_PAGE,
|
||||
UPLOAD_IMAGE,
|
||||
UPLOAD_ATTACHMENT,
|
||||
UPDATE_COMMENT,
|
||||
DELETE_COMMENT
|
||||
}
|
||||
|
||||
@Entity(
|
||||
tableName = "pending_operations",
|
||||
indices = [
|
||||
Index(value = ["entityId"]),
|
||||
Index(value = ["entityType"]),
|
||||
Index(value = ["type"])
|
||||
]
|
||||
)
|
||||
data class PendingOperation(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
val type: OperationType,
|
||||
val entityType: String,
|
||||
val entityId: String?,
|
||||
val payload: String,
|
||||
val timestamp: Long,
|
||||
val retryCount: Int = 0,
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
val tempId: String? = null
|
||||
)
|
||||
|
||||
class Converters {
|
||||
private val gson = Gson()
|
||||
|
||||
@TypeConverter
|
||||
fun fromOperationType(value: OperationType): String {
|
||||
return value.name
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toOperationType(value: String): OperationType {
|
||||
return OperationType.valueOf(value)
|
||||
}
|
||||
}
|
||||
49
app/src/main/java/com/docmost/app/data/SyncQueueDao.kt
Normal file
49
app/src/main/java/com/docmost/app/data/SyncQueueDao.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.docmost.app.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SyncQueueDao {
|
||||
@Query("SELECT * FROM pending_operations ORDER BY timestamp ASC")
|
||||
fun getAllOperations(): Flow<List<PendingOperation>>
|
||||
|
||||
@Query("SELECT * FROM pending_operations ORDER BY timestamp ASC")
|
||||
suspend fun getAllOperationsList(): List<PendingOperation>
|
||||
|
||||
@Query("SELECT * FROM pending_operations WHERE entityId = :entityId AND type = :type")
|
||||
suspend fun getOperationByEntity(entityId: String, type: OperationType): PendingOperation?
|
||||
|
||||
@Query("SELECT COUNT(*) FROM pending_operations")
|
||||
suspend fun getPendingCount(): Int
|
||||
|
||||
@Query("SELECT COUNT(*) FROM pending_operations")
|
||||
fun observePendingCount(): Flow<Int>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertOperation(operation: PendingOperation): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertOperations(operations: List<PendingOperation>)
|
||||
|
||||
@Update
|
||||
suspend fun updateOperation(operation: PendingOperation)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteOperation(operation: PendingOperation)
|
||||
|
||||
@Query("DELETE FROM pending_operations WHERE id = :id")
|
||||
suspend fun deleteOperationById(id: Long)
|
||||
|
||||
@Query("DELETE FROM pending_operations WHERE entityId = :entityId")
|
||||
suspend fun deleteOperationsByEntity(entityId: String)
|
||||
|
||||
@Query("DELETE FROM pending_operations WHERE entityType = :entityType")
|
||||
suspend fun deleteOperationsByEntityType(entityType: String)
|
||||
|
||||
@Query("DELETE FROM pending_operations")
|
||||
suspend fun deleteAll()
|
||||
|
||||
@Query("DELETE FROM pending_operations WHERE retryCount >= 3")
|
||||
suspend fun deleteFailedOperations()
|
||||
}
|
||||
36
app/src/main/java/com/docmost/app/data/SyncQueueDatabase.kt
Normal file
36
app/src/main/java/com/docmost/app/data/SyncQueueDatabase.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.docmost.app.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
|
||||
@Database(
|
||||
entities = [PendingOperation::class],
|
||||
version = 2,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class SyncQueueDatabase : RoomDatabase() {
|
||||
abstract fun syncQueueDao(): SyncQueueDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: SyncQueueDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): SyncQueueDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
SyncQueueDatabase::class.java,
|
||||
"sync_queue_database"
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.docmost.app.ui
|
||||
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
class GradientBorderDrawable : Drawable() {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val clipPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
||||
}
|
||||
|
||||
private var colorStart = Color.TRANSPARENT
|
||||
private var colorEnd = Color.TRANSPARENT
|
||||
private var strokeWidth = 0f
|
||||
private var cornerRadius = 0f
|
||||
|
||||
private var gradientShader: Shader? = null
|
||||
|
||||
fun setStroke(strokeWidth: Float, cornerRadius: Float) {
|
||||
this.strokeWidth = strokeWidth
|
||||
this.cornerRadius = cornerRadius
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
fun setGradientColors(start: Int, end: Int) {
|
||||
this.colorStart = start
|
||||
this.colorEnd = end
|
||||
gradientShader = null
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.setBounds(left, top, right, bottom)
|
||||
gradientShader = null
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val b = bounds
|
||||
if (b.width() <= 0 || b.height() <= 0) return
|
||||
|
||||
if (gradientShader == null) {
|
||||
gradientShader = LinearGradient(
|
||||
0f, 0f, b.width().toFloat(), 0f,
|
||||
colorStart, colorEnd, Shader.TileMode.CLAMP
|
||||
)
|
||||
}
|
||||
|
||||
paint.shader = gradientShader
|
||||
paint.style = Paint.Style.FILL
|
||||
|
||||
val saved = canvas.saveLayer(b.left.toFloat(), b.top.toFloat(), b.right.toFloat(), b.bottom.toFloat(), null)
|
||||
|
||||
canvas.drawRoundRect(
|
||||
0f, 0f, b.width().toFloat(), b.height().toFloat(),
|
||||
cornerRadius, cornerRadius, paint
|
||||
)
|
||||
|
||||
val inset = strokeWidth
|
||||
canvas.drawRoundRect(
|
||||
inset, inset,
|
||||
b.width() - inset, b.height() - inset,
|
||||
maxOf(0f, cornerRadius - inset), maxOf(0f, cornerRadius - inset),
|
||||
clipPaint
|
||||
)
|
||||
|
||||
canvas.restoreToCount(saved)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
paint.alpha = alpha
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
paint.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
|
||||
}
|
||||
95
app/src/main/java/com/docmost/app/ui/LoginActivity.kt
Normal file
95
app/src/main/java/com/docmost/app/ui/LoginActivity.kt
Normal file
@@ -0,0 +1,95 @@
|
||||
package com.docmost.app.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.docmost.app.R
|
||||
import com.docmost.app.api.DocmostApi
|
||||
import com.docmost.app.data.Credentials
|
||||
import com.docmost.app.data.SessionManager
|
||||
import com.docmost.app.databinding.ActivityLoginBinding
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityLoginBinding
|
||||
private lateinit var sessionManager: SessionManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.hide()
|
||||
|
||||
sessionManager = SessionManager(this)
|
||||
|
||||
if (sessionManager.hasSession()) {
|
||||
navigateToSpaces()
|
||||
return
|
||||
}
|
||||
|
||||
val saved = sessionManager.getCredentials()
|
||||
if (saved != null) {
|
||||
binding.etUrl.setText(saved.url)
|
||||
binding.etEmail.setText(saved.email)
|
||||
binding.etPassword.setText(saved.password)
|
||||
}
|
||||
|
||||
binding.btnConnect.setOnClickListener {
|
||||
val url = binding.etUrl.text.toString().trim()
|
||||
val email = binding.etEmail.text.toString().trim()
|
||||
val password = binding.etPassword.text.toString()
|
||||
|
||||
if (url.isBlank() || email.isBlank() || password.isBlank()) {
|
||||
Toast.makeText(this, R.string.fill_all_fields, Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
doLogin(url, email, password)
|
||||
}
|
||||
}
|
||||
|
||||
private fun doLogin(url: String, email: String, password: String) {
|
||||
binding.btnConnect.isEnabled = false
|
||||
binding.btnConnect.text = getString(R.string.connecting)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val api = DocmostApi(url)
|
||||
val token = api.login(email, password)
|
||||
|
||||
val user = api.getCurrentUser()
|
||||
val avatarUrl = user?.avatarUrl
|
||||
|
||||
val credentials = Credentials(url, email, password, token, avatarUrl)
|
||||
sessionManager.saveCredentials(credentials)
|
||||
sessionManager.saveAccount(credentials)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
navigateToSpaces()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.btnConnect.isEnabled = true
|
||||
binding.btnConnect.text = getString(R.string.connect)
|
||||
Toast.makeText(
|
||||
this@LoginActivity,
|
||||
"Error: ${e.message}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToSpaces() {
|
||||
val intent = Intent(this, SpacesActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
2657
app/src/main/java/com/docmost/app/ui/PageEditorActivity.kt
Normal file
2657
app/src/main/java/com/docmost/app/ui/PageEditorActivity.kt
Normal file
File diff suppressed because it is too large
Load Diff
2362
app/src/main/java/com/docmost/app/ui/SpacesActivity.kt
Normal file
2362
app/src/main/java/com/docmost/app/ui/SpacesActivity.kt
Normal file
File diff suppressed because it is too large
Load Diff
526
app/src/main/java/com/docmost/app/ui/SpacesAdapter.kt
Normal file
526
app/src/main/java/com/docmost/app/ui/SpacesAdapter.kt
Normal file
@@ -0,0 +1,526 @@
|
||||
package com.docmost.app.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.docmost.app.R
|
||||
import com.docmost.app.api.AttachmentType
|
||||
import com.docmost.app.api.Page
|
||||
import com.docmost.app.api.Space
|
||||
import com.docmost.app.BuildConfig
|
||||
import com.google.android.material.color.MaterialColors
|
||||
|
||||
class SpacesAdapter(
|
||||
private val onSpaceToggle: (Space) -> Unit,
|
||||
private val onPageClick: (Page) -> Unit,
|
||||
private val onSpaceEdit: (Space) -> Unit,
|
||||
private val onAddPage: (Space) -> Unit,
|
||||
private val onPageDelete: (Page) -> Unit,
|
||||
private val onPageLongClick: (Page) -> Unit,
|
||||
private val onPageToggle: (Page) -> Unit,
|
||||
private val baseUrl: String,
|
||||
var currentUserId: String = ""
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
companion object {
|
||||
private const val TYPE_HEADER = 0
|
||||
private const val TYPE_SPACE = 1
|
||||
}
|
||||
|
||||
private sealed class DisplayItem {
|
||||
data class Header(val label: String) : DisplayItem()
|
||||
data class SpaceItem(val space: Space) : DisplayItem()
|
||||
}
|
||||
|
||||
private val displayItems = mutableListOf<DisplayItem>()
|
||||
|
||||
private val spaces = mutableListOf<Space>()
|
||||
private val pagesMap = mutableMapOf<String, List<Page>>()
|
||||
private val expandedSpaces = mutableSetOf<String>()
|
||||
private val childPagesMap = mutableMapOf<String, List<Page>>()
|
||||
private val expandedPages = mutableSetOf<String>()
|
||||
private var animateEntry = false
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (displayItems[position]) {
|
||||
is DisplayItem.Header -> TYPE_HEADER
|
||||
is DisplayItem.SpaceItem -> TYPE_SPACE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
TYPE_HEADER -> {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_space_header, parent, false)
|
||||
HeaderViewHolder(view)
|
||||
}
|
||||
else -> {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_space, parent, false)
|
||||
SpaceViewHolder(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = displayItems[position]) {
|
||||
is DisplayItem.Header -> (holder as HeaderViewHolder).bind(item.label)
|
||||
is DisplayItem.SpaceItem -> (holder as SpaceViewHolder).bind(item.space)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = displayItems.size
|
||||
|
||||
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
|
||||
super.onViewAttachedToWindow(holder)
|
||||
if (!animateEntry) return
|
||||
|
||||
val pos = holder.absoluteAdapterPosition
|
||||
if (pos < 0) return
|
||||
|
||||
val density = holder.itemView.resources.displayMetrics.density
|
||||
holder.itemView.apply {
|
||||
alpha = 0f
|
||||
translationY = 40 * density
|
||||
postDelayed({
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(400)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.start()
|
||||
}, (pos * 60L).coerceAtMost(500L))
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDisplayItems() {
|
||||
displayItems.clear()
|
||||
|
||||
val effectiveUserId = currentUserId.ifBlank {
|
||||
spaces.firstNotNullOfOrNull { it.membership?.userId } ?: ""
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) android.util.Log.d("SpacesAdapter", "buildDisplayItems: currentUserId='$currentUserId', effectiveUserId='$effectiveUserId', spaces count=${spaces.size}")
|
||||
for (space in spaces) {
|
||||
if (BuildConfig.DEBUG) android.util.Log.d("SpacesAdapter", " space='${space.name}' creatorId='${space.creatorId}' membership=${space.membership}")
|
||||
}
|
||||
|
||||
val ownSpaces = spaces.filter {
|
||||
it.creatorId == effectiveUserId
|
||||
}
|
||||
val sharedSpaces = spaces.filter {
|
||||
effectiveUserId.isNotBlank() && it.creatorId != effectiveUserId
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) android.util.Log.d("SpacesAdapter", "ownSpaces=${ownSpaces.size}, sharedSpaces=${sharedSpaces.size}")
|
||||
|
||||
if (ownSpaces.isNotEmpty()) {
|
||||
displayItems.add(DisplayItem.Header("Tus espacios"))
|
||||
for (space in ownSpaces) {
|
||||
displayItems.add(DisplayItem.SpaceItem(space))
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedSpaces.isNotEmpty()) {
|
||||
displayItems.add(DisplayItem.Header("Compartidos contigo"))
|
||||
for (space in sharedSpaces) {
|
||||
displayItems.add(DisplayItem.SpaceItem(space))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun submitList(
|
||||
spaces: List<Space>,
|
||||
pagesMap: Map<String, List<Page>>,
|
||||
expandedSpaces: Set<String>,
|
||||
childPagesMap: Map<String, List<Page>> = emptyMap(),
|
||||
expandedPages: Set<String> = emptySet()
|
||||
) {
|
||||
val hadItems = this.spaces.isNotEmpty()
|
||||
this.spaces.clear()
|
||||
this.spaces.addAll(spaces)
|
||||
this.pagesMap.clear()
|
||||
this.pagesMap.putAll(pagesMap)
|
||||
this.expandedSpaces.clear()
|
||||
this.expandedSpaces.addAll(expandedSpaces)
|
||||
this.childPagesMap.clear()
|
||||
this.childPagesMap.putAll(childPagesMap)
|
||||
this.expandedPages.clear()
|
||||
this.expandedPages.addAll(expandedPages)
|
||||
|
||||
buildDisplayItems()
|
||||
|
||||
animateEntry = !hadItems && this.spaces.isNotEmpty()
|
||||
notifyDataSetChanged()
|
||||
if (animateEntry) {
|
||||
recyclerViewRef?.post { animateEntry = false }
|
||||
}
|
||||
}
|
||||
|
||||
private var recyclerViewRef: View? = null
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
recyclerViewRef = recyclerView
|
||||
}
|
||||
|
||||
inner class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val tvLabel: TextView = itemView.findViewById(R.id.tvHeaderLabel)
|
||||
|
||||
fun bind(label: String) {
|
||||
tvLabel.text = label
|
||||
}
|
||||
}
|
||||
|
||||
inner class SpaceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val tvName: TextView = itemView.findViewById(R.id.tvSpaceName)
|
||||
private val tvDesc: TextView = itemView.findViewById(R.id.tvSpaceDescription)
|
||||
private val tvRole: TextView = itemView.findViewById(R.id.tvRole)
|
||||
private val btnExpand: ImageButton = itemView.findViewById(R.id.btnExpand)
|
||||
private val btnEdit: ImageButton = itemView.findViewById(R.id.btnEditSpace)
|
||||
private val ivIcon: ImageView = itemView.findViewById(R.id.ivSpaceIcon)
|
||||
private val ivBlurredBg: ImageView = itemView.findViewById(R.id.ivBlurredBg)
|
||||
private val vScrim: View = itemView.findViewById(R.id.vScrim)
|
||||
private val ivGradientBorder: View = itemView.findViewById(R.id.ivGradientBorder)
|
||||
private val pagesContainer: LinearLayout = itemView.findViewById(R.id.pagesContainer)
|
||||
private val tvPageCount: TextView = itemView.findViewById(R.id.tvPageCount)
|
||||
private var currentSpace: Space? = null
|
||||
private var wasExpanded = false
|
||||
private var isCollapsing = false
|
||||
private val cardView: com.google.android.material.card.MaterialCardView =
|
||||
itemView as com.google.android.material.card.MaterialCardView
|
||||
|
||||
private val gradientBorderDrawable = GradientBorderDrawable()
|
||||
private val density = itemView.resources.displayMetrics.density
|
||||
|
||||
init {
|
||||
ivGradientBorder.background = gradientBorderDrawable
|
||||
gradientBorderDrawable.setStroke(
|
||||
2 * density,
|
||||
12 * density
|
||||
)
|
||||
val defaultStart = MaterialColors.getColor(
|
||||
itemView, com.google.android.material.R.attr.colorPrimaryContainer
|
||||
)
|
||||
val defaultEnd = MaterialColors.getColor(
|
||||
itemView, com.google.android.material.R.attr.colorSecondaryContainer
|
||||
)
|
||||
gradientBorderDrawable.setGradientColors(defaultStart, defaultEnd)
|
||||
}
|
||||
|
||||
private fun extractGradientColors(bitmap: Bitmap): Pair<Int, Int>? {
|
||||
return try {
|
||||
val software = if (bitmap.config == Bitmap.Config.HARDWARE) {
|
||||
bitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
} else {
|
||||
bitmap
|
||||
} ?: return null
|
||||
|
||||
val size = minOf(software.width, software.height, 50)
|
||||
val scaled = Bitmap.createScaledBitmap(software, size, size, true)
|
||||
val colors = mutableListOf<Int>()
|
||||
val step = maxOf(1, size / 10)
|
||||
for (x in 0 until size step step) {
|
||||
for (y in 0 until size step step) {
|
||||
colors.add(scaled.getPixel(x, y))
|
||||
}
|
||||
}
|
||||
if (colors.size < 2) return null
|
||||
|
||||
colors.sortBy { color ->
|
||||
0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)
|
||||
}
|
||||
|
||||
Pair(colors.first(), colors.last())
|
||||
} catch (e: Exception) {
|
||||
if (BuildConfig.DEBUG) android.util.Log.e("SpacesAdapter", "extractGradientColors failed: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRoleLabel(role: String): String {
|
||||
return when (role.lowercase()) {
|
||||
"admin" -> "Admin"
|
||||
"writer" -> "Editor"
|
||||
"reader" -> "Visor"
|
||||
else -> role
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(space: Space) {
|
||||
currentSpace = space
|
||||
tvName.text = space.name
|
||||
if (space.description.isNullOrBlank()) {
|
||||
tvDesc.visibility = View.GONE
|
||||
} else {
|
||||
tvDesc.visibility = View.VISIBLE
|
||||
tvDesc.text = space.description
|
||||
}
|
||||
|
||||
val isShared = currentUserId.isNotBlank() && space.creatorId != currentUserId
|
||||
if (isShared && space.membership?.role.isNullOrBlank().not()) {
|
||||
tvRole.visibility = View.VISIBLE
|
||||
tvRole.text = getRoleLabel(space.membership!!.role)
|
||||
} else {
|
||||
tvRole.visibility = View.GONE
|
||||
}
|
||||
|
||||
val isExpanded = expandedSpaces.contains(space.id)
|
||||
btnExpand.setImageResource(
|
||||
if (isExpanded) R.drawable.ic_expand_less else R.drawable.ic_expand_more
|
||||
)
|
||||
|
||||
itemView.setOnClickListener { onSpaceToggle(space) }
|
||||
btnExpand.setOnClickListener { onSpaceToggle(space) }
|
||||
btnEdit.setOnClickListener { onSpaceEdit(space) }
|
||||
|
||||
val pages = pagesMap[space.id]
|
||||
if (isExpanded || pages == null) {
|
||||
tvPageCount.visibility = View.GONE
|
||||
} else {
|
||||
tvPageCount.visibility = View.VISIBLE
|
||||
tvPageCount.text = "${pages.size} páginas"
|
||||
}
|
||||
|
||||
if (isExpanded && !wasExpanded) {
|
||||
pagesContainer.visibility = View.VISIBLE
|
||||
pagesContainer.alpha = 1f
|
||||
renderPages()
|
||||
pagesContainer.post { animatePageEntries() }
|
||||
} else if (!isExpanded && wasExpanded) {
|
||||
animatePageExit()
|
||||
} else if (isExpanded) {
|
||||
pagesContainer.visibility = View.VISIBLE
|
||||
pagesContainer.alpha = 1f
|
||||
renderPages()
|
||||
} else {
|
||||
pagesContainer.visibility = View.GONE
|
||||
pagesContainer.removeAllViews()
|
||||
}
|
||||
|
||||
wasExpanded = isExpanded
|
||||
loadSpaceIcon(space)
|
||||
}
|
||||
|
||||
fun animateCollapse(onDone: () -> Unit) {
|
||||
if (isCollapsing) return
|
||||
isCollapsing = true
|
||||
|
||||
val density = itemView.resources.displayMetrics.density
|
||||
val childCount = pagesContainer.childCount
|
||||
if (childCount == 0) {
|
||||
isCollapsing = false
|
||||
onDone()
|
||||
return
|
||||
}
|
||||
|
||||
var remaining = childCount
|
||||
for (i in 0 until childCount) {
|
||||
pagesContainer.getChildAt(i).animate()
|
||||
.alpha(0f)
|
||||
.translationY(-20 * density)
|
||||
.setDuration(200)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.withEndAction {
|
||||
remaining--
|
||||
if (remaining == 0) {
|
||||
isCollapsing = false
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun animatePageEntries() {
|
||||
val density = itemView.resources.displayMetrics.density
|
||||
for (i in 0 until pagesContainer.childCount) {
|
||||
val child = pagesContainer.getChildAt(i)
|
||||
child.alpha = 0f
|
||||
child.translationY = 30 * density
|
||||
child.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(350)
|
||||
.setStartDelay((i * 60L).coerceAtMost(400L))
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun animatePageExit() {
|
||||
val childCount = pagesContainer.childCount
|
||||
if (childCount == 0) {
|
||||
pagesContainer.visibility = View.GONE
|
||||
pagesContainer.removeAllViews()
|
||||
return
|
||||
}
|
||||
|
||||
if (pagesContainer.getChildAt(0).alpha == 0f) {
|
||||
pagesContainer.visibility = View.GONE
|
||||
pagesContainer.removeAllViews()
|
||||
return
|
||||
}
|
||||
|
||||
var remaining = childCount
|
||||
for (i in 0 until childCount) {
|
||||
pagesContainer.getChildAt(i).animate()
|
||||
.alpha(0f)
|
||||
.setDuration(150)
|
||||
.setInterpolator(DecelerateInterpolator())
|
||||
.withEndAction {
|
||||
remaining--
|
||||
if (remaining == 0) {
|
||||
pagesContainer.visibility = View.GONE
|
||||
pagesContainer.removeAllViews()
|
||||
}
|
||||
}
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderPages() {
|
||||
pagesContainer.removeAllViews()
|
||||
|
||||
val space = currentSpace ?: return
|
||||
val pages = pagesMap[space.id]
|
||||
|
||||
if (pages.isNullOrEmpty()) {
|
||||
val addPageView = createAddPageView()
|
||||
pagesContainer.addView(addPageView)
|
||||
return
|
||||
}
|
||||
|
||||
for (page in pages) {
|
||||
renderPageWithChildren(page, 0)
|
||||
}
|
||||
|
||||
val addPageView = createAddPageView()
|
||||
pagesContainer.addView(addPageView)
|
||||
}
|
||||
|
||||
private fun renderPageWithChildren(page: Page, depth: Int) {
|
||||
val pageView = createPageView(page, depth)
|
||||
pagesContainer.addView(pageView)
|
||||
|
||||
if (expandedPages.contains(page.id)) {
|
||||
val children = childPagesMap[page.id]
|
||||
if (!children.isNullOrEmpty()) {
|
||||
for (child in children) {
|
||||
renderPageWithChildren(child, depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPageView(page: Page, depth: Int): View {
|
||||
val view = LayoutInflater.from(itemView.context).inflate(R.layout.item_page, pagesContainer, false)
|
||||
val tvTitle: TextView = view.findViewById(R.id.tvPageTitle)
|
||||
val tvIcon: TextView = view.findViewById(R.id.tvPageIcon)
|
||||
val ivIcon: ImageView = view.findViewById(R.id.ivPageIcon)
|
||||
val btnDelete: ImageButton = view.findViewById(R.id.btnDeletePage)
|
||||
val btnExpand: ImageButton = view.findViewById(R.id.btnExpandPage)
|
||||
|
||||
tvTitle.text = if (page.title.isNullOrBlank()) "Untitled" else page.title
|
||||
|
||||
if (page.icon.isNullOrBlank()) {
|
||||
tvIcon.visibility = View.GONE
|
||||
ivIcon.visibility = View.VISIBLE
|
||||
} else {
|
||||
tvIcon.visibility = View.VISIBLE
|
||||
tvIcon.text = page.icon
|
||||
ivIcon.visibility = View.GONE
|
||||
}
|
||||
|
||||
val density = itemView.resources.displayMetrics.density
|
||||
val basePadding = 24
|
||||
val paddingStart = ((basePadding + depth * 24) * density).toInt()
|
||||
view.setPadding(paddingStart, view.paddingTop, view.paddingRight, view.paddingBottom)
|
||||
|
||||
if (page.hasChildren) {
|
||||
btnExpand.visibility = View.VISIBLE
|
||||
val isExpanded = expandedPages.contains(page.id)
|
||||
btnExpand.setImageResource(
|
||||
if (isExpanded) R.drawable.ic_expand_less else R.drawable.ic_expand_more
|
||||
)
|
||||
btnExpand.setOnClickListener {
|
||||
onPageToggle(page)
|
||||
}
|
||||
} else {
|
||||
btnExpand.visibility = View.GONE
|
||||
}
|
||||
|
||||
view.setOnClickListener { onPageClick(page) }
|
||||
view.setOnLongClickListener { onPageLongClick(page); true }
|
||||
btnDelete.visibility = View.VISIBLE
|
||||
btnDelete.setOnClickListener { onPageDelete(page) }
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun createAddPageView(): View {
|
||||
val view = LayoutInflater.from(itemView.context).inflate(R.layout.item_add_page, pagesContainer, false)
|
||||
currentSpace?.let { space -> view.setOnClickListener { onAddPage(space) } }
|
||||
return view
|
||||
}
|
||||
|
||||
private fun loadSpaceIcon(space: Space) {
|
||||
if (space.logo.isNullOrBlank()) {
|
||||
ivIcon.visibility = View.GONE
|
||||
ivIcon.setImageDrawable(null)
|
||||
ivBlurredBg.visibility = View.GONE
|
||||
vScrim.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
ivIcon.visibility = View.VISIBLE
|
||||
val url = buildString {
|
||||
append(baseUrl.trimEnd('/'))
|
||||
append("/api/attachments/img/${AttachmentType.SPACE_ICON}/${space.logo}")
|
||||
}
|
||||
|
||||
ivBlurredBg.visibility = View.GONE
|
||||
vScrim.visibility = View.GONE
|
||||
ivIcon.setImageResource(R.drawable.ic_page)
|
||||
|
||||
com.docmost.app.utils.ImageLoader.loadWithBitmap(url, ivIcon, onBitmapLoaded = { bitmap: Bitmap ->
|
||||
extractGradientColors(bitmap)?.let { (c1, c2) ->
|
||||
gradientBorderDrawable.setGradientColors(c1, c2)
|
||||
}
|
||||
setBlurredBackground(bitmap)
|
||||
})
|
||||
}
|
||||
|
||||
private fun setBlurredBackground(bitmap: Bitmap) {
|
||||
val surfaceColor = MaterialColors.getColor(
|
||||
itemView, com.google.android.material.R.attr.colorSurface
|
||||
)
|
||||
|
||||
val scale = 6
|
||||
val smallW = maxOf(bitmap.width / scale, 1)
|
||||
val smallH = maxOf(bitmap.height / scale, 1)
|
||||
val small = Bitmap.createScaledBitmap(bitmap, smallW, smallH, true)
|
||||
val blurred = Bitmap.createScaledBitmap(small, smallW * 2, smallH * 2, true)
|
||||
|
||||
ivBlurredBg.setImageBitmap(blurred)
|
||||
ivBlurredBg.alpha = 0.35f
|
||||
ivBlurredBg.visibility = View.VISIBLE
|
||||
|
||||
vScrim.background = GradientDrawable(
|
||||
GradientDrawable.Orientation.LEFT_RIGHT,
|
||||
intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, surfaceColor, surfaceColor)
|
||||
)
|
||||
vScrim.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
260
app/src/main/java/com/docmost/app/ui/VersionViewerActivity.kt
Normal file
260
app/src/main/java/com/docmost/app/ui/VersionViewerActivity.kt
Normal file
@@ -0,0 +1,260 @@
|
||||
package com.docmost.app.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.docmost.app.R
|
||||
import com.docmost.app.api.DocmostApi
|
||||
import com.docmost.app.data.Credentials
|
||||
import com.docmost.app.data.SessionManager
|
||||
import com.docmost.app.utils.ContentConverter
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class VersionViewerActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var webView: WebView
|
||||
private lateinit var progress: CircularProgressIndicator
|
||||
private lateinit var actionBar: LinearLayout
|
||||
private lateinit var ivAvatar: ShapeableImageView
|
||||
private lateinit var tvAuthor: TextView
|
||||
private lateinit var tvDateTime: TextView
|
||||
private lateinit var btnLoadVersion: MaterialButton
|
||||
private lateinit var api: DocmostApi
|
||||
private var creds: Credentials? = null
|
||||
private var currentPageContent: String = ""
|
||||
private var originalTipTapJson: String = ""
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_version_viewer)
|
||||
|
||||
// Hide the default ActionBar
|
||||
supportActionBar?.hide()
|
||||
|
||||
webView = findViewById(R.id.webViewVersion)
|
||||
progress = findViewById(R.id.progressVersion)
|
||||
actionBar = findViewById(R.id.actionBar)
|
||||
ivAvatar = findViewById(R.id.ivAvatar)
|
||||
tvAuthor = findViewById(R.id.tvAuthor)
|
||||
tvDateTime = findViewById(R.id.tvDateTime)
|
||||
btnLoadVersion = findViewById(R.id.btnLoadVersion)
|
||||
|
||||
val pageId = intent.getStringExtra("pageId") ?: return finish()
|
||||
val historyId = intent.getStringExtra("historyId") ?: return finish()
|
||||
val author = intent.getStringExtra("author") ?: "Desconocido"
|
||||
val dateTime = intent.getStringExtra("dateTime") ?: ""
|
||||
|
||||
val sessionManager = SessionManager(this)
|
||||
com.docmost.app.utils.ImageLoader.init(this, sessionManager)
|
||||
creds = sessionManager.getCredentials()
|
||||
if (creds == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
api = DocmostApi(creds!!.url)
|
||||
api.setAuthToken(creds!!.authToken)
|
||||
|
||||
tvAuthor.text = author
|
||||
tvDateTime.text = dateTime
|
||||
|
||||
// Cargar avatar del autor
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val history = api.getPageHistoryInfo(pageId, historyId)
|
||||
val avatarUrl = history.lastUpdatedBy?.avatarUrl
|
||||
if (!avatarUrl.isNullOrBlank()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val fullAvatarUrl = "${creds?.url}/api/attachments/img/avatar/$avatarUrl"
|
||||
com.docmost.app.utils.ImageLoader.loadSync(fullAvatarUrl, ivAvatar, R.drawable.ic_page)
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
ivAvatar.setImageResource(R.drawable.ic_page)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
ivAvatar.setImageResource(R.drawable.ic_page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
btnLoadVersion.setOnClickListener {
|
||||
loadVersionInEditor(pageId, historyId)
|
||||
}
|
||||
|
||||
setupWebView(pageId, historyId)
|
||||
}
|
||||
|
||||
private fun setupWebView(pageId: String, historyId: String) {
|
||||
val isDark = when (resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) {
|
||||
android.content.res.Configuration.UI_MODE_NIGHT_YES -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val history = api.getPageHistoryInfo(pageId, historyId)
|
||||
val htmlContent = if (history.content != null) {
|
||||
val gson = Gson()
|
||||
val json = gson.toJson(history.content)
|
||||
originalTipTapJson = json
|
||||
ContentConverter.tipTapToHtml(json)
|
||||
} else {
|
||||
"<p>Sin contenido</p>"
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
webView.settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
allowFileAccess = false
|
||||
allowContentAccess = false
|
||||
builtInZoomControls = true
|
||||
displayZoomControls = false
|
||||
loadWithOverviewMode = true
|
||||
useWideViewPort = true
|
||||
}
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
progress.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
val darkModeCss = if (isDark) """
|
||||
body {
|
||||
background-color: #1a1a1a !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
.version-banner {
|
||||
background: #2d2d2d !important;
|
||||
border-color: #404040 !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
pre, code {
|
||||
background: #2d2d2d !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
blockquote {
|
||||
border-left-color: #404040 !important;
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
th {
|
||||
background: #2a2a2a !important;
|
||||
}
|
||||
td, th {
|
||||
border-color: #404040 !important;
|
||||
}
|
||||
a {
|
||||
color: #64b5f6 !important;
|
||||
}
|
||||
""" else ""
|
||||
|
||||
val fullHtml = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
padding: 16px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.version-banner {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
color: #856404;
|
||||
}
|
||||
.content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
p { margin: 8px 0; }
|
||||
h1, h2, h3, h4, h5, h6 { margin: 16px 0 8px 0; }
|
||||
ul, ol { padding-left: 20px; }
|
||||
pre { background: #f4f4f4; padding: 12px; border-radius: 4px; overflow-x: auto; }
|
||||
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
|
||||
blockquote { border-left: 3px solid #ccc; padding-left: 12px; margin: 8px 0; color: #666; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; }
|
||||
th { background: #f4f4f4; }
|
||||
a { color: #0066cc; }
|
||||
$darkModeCss
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="version-banner">
|
||||
<strong>Versión de solo lectura</strong>
|
||||
</div>
|
||||
<div class="content">
|
||||
$htmlContent
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""".trimIndent()
|
||||
|
||||
webView.loadDataWithBaseURL(
|
||||
"${creds?.url}/",
|
||||
fullHtml,
|
||||
"text/html",
|
||||
"UTF-8",
|
||||
null
|
||||
)
|
||||
|
||||
currentPageContent = htmlContent
|
||||
|
||||
// Mostrar barra de acción con info del usuario y botón
|
||||
actionBar.visibility = View.VISIBLE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
progress.visibility = View.GONE
|
||||
webView.loadData(
|
||||
"<html><body><h1>Error</h1><p>${e.message}</p></body></html>",
|
||||
"text/html",
|
||||
"UTF-8"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadVersionInEditor(pageId: String, historyId: String) {
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("loadVersion", true)
|
||||
resultIntent.putExtra("pageId", pageId)
|
||||
resultIntent.putExtra("historyId", historyId)
|
||||
resultIntent.putExtra("tipTapJson", originalTipTapJson)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
setupWebView(
|
||||
intent.getStringExtra("pageId") ?: return,
|
||||
intent.getStringExtra("historyId") ?: return
|
||||
)
|
||||
}
|
||||
}
|
||||
189
app/src/main/java/com/docmost/app/utils/ContentConverter.kt
Normal file
189
app/src/main/java/com/docmost/app/utils/ContentConverter.kt
Normal file
@@ -0,0 +1,189 @@
|
||||
package com.docmost.app.utils
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
object ContentConverter {
|
||||
private val gson = Gson()
|
||||
|
||||
fun tipTapJsonToHtml(tipTapJson: String): String {
|
||||
return try {
|
||||
val mapType = object : TypeToken<Map<String, Any?>>() {}.type
|
||||
val doc = gson.fromJson<Map<String, Any?>>(tipTapJson, mapType)
|
||||
nodeToHtml(doc)
|
||||
} catch (e: Exception) {
|
||||
tipTapJson
|
||||
}
|
||||
}
|
||||
|
||||
fun tipTapToHtml(tipTapJson: String): String {
|
||||
return try {
|
||||
val mapType = object : TypeToken<Map<String, Any?>>() {}.type
|
||||
val doc = gson.fromJson<Map<String, Any?>>(tipTapJson, mapType)
|
||||
nodeToHtml(doc)
|
||||
} catch (e: Exception) {
|
||||
tipTapJson
|
||||
}
|
||||
}
|
||||
|
||||
private fun nodeToHtml(node: Map<String, Any?>): String {
|
||||
val type = node["type"] as? String
|
||||
|
||||
if (type == "text") {
|
||||
var text = (node["text"] as? String ?: "").escapeHtml()
|
||||
val marks = node["marks"] as? List<Map<String, Any?>>
|
||||
marks?.forEach { mark ->
|
||||
when (mark["type"] as? String) {
|
||||
"bold" -> text = "<strong>$text</strong>"
|
||||
"italic" -> text = "<em>$text</em>"
|
||||
"code" -> text = "<code>$text</code>"
|
||||
"strike" -> text = "<del>$text</del>"
|
||||
"link" -> {
|
||||
val href = (mark["attrs"] as? Map<*, *>)?.get("href") as? String ?: ""
|
||||
text = "<a href=\"${href.escapeHtml()}\" target=\"_blank\">$text</a>"
|
||||
}
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
if (type == "hardBreak") {
|
||||
return "<br>"
|
||||
}
|
||||
|
||||
val sb = StringBuilder()
|
||||
val content = node["content"] as? List<*>
|
||||
|
||||
when (type) {
|
||||
"doc" -> {
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
}
|
||||
"paragraph" -> {
|
||||
sb.append("<p>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</p>")
|
||||
}
|
||||
"heading" -> {
|
||||
val level = (node["attrs"] as? Map<*, *>)?.get("level") as? Int ?: 1
|
||||
sb.append("<h$level>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</h$level>")
|
||||
}
|
||||
"bulletList" -> {
|
||||
sb.append("<ul>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</ul>")
|
||||
}
|
||||
"orderedList" -> {
|
||||
sb.append("<ol>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</ol>")
|
||||
}
|
||||
"listItem" -> {
|
||||
sb.append("<li>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</li>")
|
||||
}
|
||||
"codeBlock" -> {
|
||||
sb.append("<pre><code>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</code></pre>")
|
||||
}
|
||||
"blockquote" -> {
|
||||
sb.append("<blockquote>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</blockquote>")
|
||||
}
|
||||
"image" -> {
|
||||
val src = (node["attrs"] as? Map<*, *>)?.get("src") as? String ?: ""
|
||||
val alt = (node["attrs"] as? Map<*, *>)?.get("alt") as? String ?: ""
|
||||
sb.append("<img src=\"${src.escapeHtml()}\" alt=\"${alt.escapeHtml()}\">")
|
||||
}
|
||||
"table" -> {
|
||||
sb.append("<table>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</table>")
|
||||
}
|
||||
"tableRow" -> {
|
||||
sb.append("<tr>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</tr>")
|
||||
}
|
||||
"tableCell", "tableHeader" -> {
|
||||
val tag = if (type == "tableHeader") "th" else "td"
|
||||
sb.append("<$tag>")
|
||||
content?.forEach { child ->
|
||||
if (child is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
sb.append(nodeToHtml(child as Map<String, Any?>))
|
||||
}
|
||||
}
|
||||
sb.append("</$tag>")
|
||||
}
|
||||
"horizontalRule" -> {
|
||||
sb.append("<hr>")
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun String.escapeHtml(): String {
|
||||
return this
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'")
|
||||
}
|
||||
}
|
||||
144
app/src/main/java/com/docmost/app/utils/ImageLoader.kt
Normal file
144
app/src/main/java/com/docmost/app/utils/ImageLoader.kt
Normal file
@@ -0,0 +1,144 @@
|
||||
package com.docmost.app.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import coil.ImageLoader
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import coil.request.ImageRequest
|
||||
import com.docmost.app.R
|
||||
import com.docmost.app.data.SessionManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
object ImageLoader {
|
||||
|
||||
private var instance: ImageLoader? = null
|
||||
private var sessionManager: SessionManager? = null
|
||||
|
||||
fun init(context: Context, sessionManager: SessionManager) {
|
||||
this.sessionManager = sessionManager
|
||||
val cacheDir = File(context.cacheDir, "image_cache")
|
||||
|
||||
instance = ImageLoader.Builder(context)
|
||||
.memoryCache {
|
||||
MemoryCache.Builder(context)
|
||||
.maxSizePercent(0.25)
|
||||
.build()
|
||||
}
|
||||
.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(cacheDir)
|
||||
.maxSizePercent(0.25)
|
||||
.build()
|
||||
}
|
||||
.okHttpClient {
|
||||
okhttp3.OkHttpClient.Builder()
|
||||
.addInterceptor { chain ->
|
||||
val original = chain.request()
|
||||
val creds = sessionManager.getCredentials()
|
||||
val newRequest = if (creds?.authToken != null) {
|
||||
original.newBuilder()
|
||||
.addHeader("Cookie", "authToken=${creds.authToken}")
|
||||
.build()
|
||||
} else {
|
||||
original
|
||||
}
|
||||
chain.proceed(newRequest)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun load(
|
||||
url: String,
|
||||
imageView: ImageView,
|
||||
fallback: Int = R.drawable.ic_page
|
||||
) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val loader = instance ?: run {
|
||||
imageView.setImageResource(fallback)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val request = ImageRequest.Builder(imageView.context)
|
||||
.data(url)
|
||||
.target(
|
||||
onSuccess = { result ->
|
||||
imageView.setImageDrawable(result)
|
||||
},
|
||||
onError = {
|
||||
imageView.setImageResource(fallback)
|
||||
}
|
||||
)
|
||||
.build()
|
||||
|
||||
loader.enqueue(request)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSync(
|
||||
url: String,
|
||||
imageView: ImageView,
|
||||
fallback: Int = R.drawable.ic_page
|
||||
) {
|
||||
val loader = instance ?: run {
|
||||
imageView.setImageResource(fallback)
|
||||
return
|
||||
}
|
||||
|
||||
val request = ImageRequest.Builder(imageView.context)
|
||||
.data(url)
|
||||
.target(
|
||||
onSuccess = { result ->
|
||||
imageView.setImageDrawable(result)
|
||||
},
|
||||
onError = {
|
||||
imageView.setImageResource(fallback)
|
||||
}
|
||||
)
|
||||
.build()
|
||||
|
||||
loader.enqueue(request)
|
||||
}
|
||||
|
||||
fun loadWithBitmap(
|
||||
url: String,
|
||||
imageView: ImageView,
|
||||
onBitmapLoaded: ((Bitmap) -> Unit)? = null,
|
||||
fallback: Int = R.drawable.ic_page
|
||||
) {
|
||||
val loader = instance ?: run {
|
||||
imageView.setImageResource(fallback)
|
||||
return
|
||||
}
|
||||
|
||||
val request = ImageRequest.Builder(imageView.context)
|
||||
.data(url)
|
||||
.target(
|
||||
onSuccess = { result ->
|
||||
imageView.setImageDrawable(result)
|
||||
val bitmap = drawableToBitmap(result)
|
||||
bitmap?.let { onBitmapLoaded?.invoke(it) }
|
||||
},
|
||||
onError = {
|
||||
imageView.setImageResource(fallback)
|
||||
}
|
||||
)
|
||||
.build()
|
||||
|
||||
loader.enqueue(request)
|
||||
}
|
||||
|
||||
private fun drawableToBitmap(drawable: Drawable): Bitmap? {
|
||||
if (drawable is BitmapDrawable) {
|
||||
return drawable.bitmap
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.docmost.app.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
enum class NetworkStatus {
|
||||
ONLINE,
|
||||
OFFLINE,
|
||||
SYNCING
|
||||
}
|
||||
|
||||
class NetworkConnectivityObserver(private val context: Context) {
|
||||
|
||||
private val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
private val _networkStatus = MutableStateFlow(checkNetworkStatus())
|
||||
val networkStatus: StateFlow<NetworkStatus> = _networkStatus.asStateFlow()
|
||||
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
_networkStatus.value = NetworkStatus.ONLINE
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
_networkStatus.value = NetworkStatus.OFFLINE
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
val isOnline = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
_networkStatus.value = if (isOnline) NetworkStatus.ONLINE else NetworkStatus.OFFLINE
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
registerNetworkCallback()
|
||||
}
|
||||
|
||||
private fun registerNetworkCallback() {
|
||||
val networkRequest = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
|
||||
}
|
||||
|
||||
private fun checkNetworkStatus(): NetworkStatus {
|
||||
return try {
|
||||
val network = connectivityManager.activeNetwork
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
val hasInternet = capabilities?.hasCapability(
|
||||
NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||
) == true
|
||||
if (hasInternet) NetworkStatus.ONLINE else NetworkStatus.OFFLINE
|
||||
} catch (e: Exception) {
|
||||
NetworkStatus.OFFLINE
|
||||
}
|
||||
}
|
||||
|
||||
fun isOnline(): Boolean {
|
||||
return _networkStatus.value == NetworkStatus.ONLINE
|
||||
}
|
||||
|
||||
fun isOffline(): Boolean {
|
||||
return _networkStatus.value == NetworkStatus.OFFLINE
|
||||
}
|
||||
|
||||
fun setSyncing(isSyncing: Boolean) {
|
||||
_networkStatus.value = if (isSyncing) NetworkStatus.SYNCING else checkNetworkStatus()
|
||||
}
|
||||
|
||||
fun observeNetworkStatus(): Flow<NetworkStatus> = callbackFlow {
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.ONLINE)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.OFFLINE)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
val isOnline = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
trySend(if (isOnline) NetworkStatus.ONLINE else NetworkStatus.OFFLINE)
|
||||
}
|
||||
}
|
||||
|
||||
val networkRequest = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(networkRequest, callback)
|
||||
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(callback)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
fun cleanup() {
|
||||
try {
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
} catch (e: Exception) {
|
||||
// Already unregistered
|
||||
}
|
||||
}
|
||||
}
|
||||
6
app/src/main/res/drawable/bg_search.xml
Normal file
6
app/src/main/res/drawable/bg_search.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?attr/colorSurfaceVariant" />
|
||||
<corners android:radius="28dp" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/circle_background.xml
Normal file
5
app/src/main/res/drawable/circle_background.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
</shape>
|
||||
10
app/src/main/res/drawable/ic_add.xml
Normal file
10
app/src/main/res/drawable/ic_add.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_delete.xml
Normal file
10
app/src/main/res/drawable/ic_delete.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnSurfaceVariant">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_docmost.xml
Normal file
10
app/src/main/res/drawable/ic_docmost.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorPrimary">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM17,17L7,17v-2h10v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z" />
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_expand_less.xml
Normal file
9
app/src/main/res/drawable/ic_expand_less.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_expand_more.xml
Normal file
9
app/src/main/res/drawable/ic_expand_more.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurface"
|
||||
android:pathData="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/>
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_link.xml
Normal file
11
app/src/main/res/drawable/ic_link.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@android:color/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4V7H7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9H7c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2H8v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4V17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z" />
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_page.xml
Normal file
9
app/src/main/res/drawable/ic_page.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8l-6,-6zM6,20V4h7v5h5v11H6z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_pencil.xml
Normal file
10
app/src/main/res/drawable/ic_pencil.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_recent.xml
Normal file
11
app/src/main/res/drawable/ic_recent.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@android:color/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9H1l3.89,3.89l0.07,0.14L9,12H6c0,-3.87 3.13,-7 7,-7s7,3.13 7,7s-3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54l0.72,-1.21l-3.5,-2.08V8H12z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_search.xml
Normal file
10
app/src/main/res/drawable/ic_search.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_send.xml
Normal file
10
app/src/main/res/drawable/ic_send.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M2.01,21L23,12L2.01,3L2,10L17,12L2,14L2.01,21Z"/>
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_settings.xml
Normal file
11
app/src/main/res/drawable/ic_settings.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@android:color/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" />
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_spaces.xml
Normal file
11
app/src/main/res/drawable/ic_spaces.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@android:color/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,4h7v7H4V4zM4,13h7v7H4v-7zM13,4h7v7h-7V4zM13,13h7v7h-7v-7z" />
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_visibility.xml
Normal file
9
app/src/main/res/drawable/ic_visibility.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorOnSurfaceVariant"
|
||||
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
||||
</vector>
|
||||
100
app/src/main/res/layout/activity_login.xml
Normal file
100
app/src/main/res/layout/activity_login.xml
Normal file
@@ -0,0 +1,100 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal"
|
||||
android:padding="24dp">
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/ic_docmost"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/app_subtitle"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/url_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:autofillHints="url" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/email_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etEmail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textEmailAddress"
|
||||
android:autofillHints="emailAddress" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:hint="@string/password_hint"
|
||||
app:passwordToggleEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:autofillHints="password" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnConnect"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:text="@string/connect"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
569
app/src/main/res/layout/activity_page_editor.xml
Normal file
569
app/src/main/res/layout/activity_page_editor.xml
Normal file
@@ -0,0 +1,569 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorPrimaryContainer"
|
||||
app:tabTextColor="?attr/colorOnPrimaryContainer"
|
||||
app:tabIndicatorColor="?attr/colorOnPrimaryContainer"
|
||||
app:tabSelectedTextColor="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/readOnlyTabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorPrimaryContainer"
|
||||
app:tabTextColor="?attr/colorOnPrimaryContainer"
|
||||
app:tabIndicatorColor="?attr/colorOnPrimaryContainer"
|
||||
app:tabSelectedTextColor="?attr/colorOnPrimaryContainer"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/readOnlyViewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/editorContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/pageContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webViewPreview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/editorLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/titleContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmoji"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:gravity="center"
|
||||
android:textSize="28sp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:text="@string/add_emoji"
|
||||
android:contentDescription="@string/choose_emoji" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/title_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="3" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webViewEditor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToOutline="false" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvOfflineBanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/offline_banner"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnPrimaryContainer"
|
||||
android:background="?attr/colorPrimaryContainer"
|
||||
android:gravity="center"
|
||||
android:padding="6dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/commentsContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvComments"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:padding="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyComments"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_comments"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressComments"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:background="?attr/colorSurfaceVariant">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/commentInputLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/write_comment"
|
||||
app:boxStrokeWidth="1dp"
|
||||
app:boxStrokeWidthFocused="1dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etCommentInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:textSize="14sp"
|
||||
android:maxLines="3" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSendComment"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_gravity="center"
|
||||
android:insetLeft="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:padding="0dp"
|
||||
app:icon="@drawable/ic_send"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="20dp"
|
||||
app:cornerRadius="24dp"
|
||||
style="@style/Widget.Material3.Button.TonalButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/indexContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvIndex"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyIndex"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/no_headings"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressIndex"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabToggleMode"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_pencil"
|
||||
android:contentDescription="@string/edit"
|
||||
android:visibility="gone"
|
||||
app:fabSize="normal" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/optionsContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/infoHeaderCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnToggleInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="ℹ️ Información"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:padding="12dp"
|
||||
android:drawableEnd="@drawable/ic_expand_more"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/infoCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Creado por:"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/creatorInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivCreator"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/circle_background" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCreator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
android:background="?attr/colorOutline" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Última actualización por:"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/updaterInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivUpdater"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/circle_background" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUpdater"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
android:background="?attr/colorOutline" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="📊 Estadísticas:"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvWordCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textSize="13sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCharCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="13sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCreatedAt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="13sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUpdatedAt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="13sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<View
|
||||
android:id="@+id/labelsDivider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
android:background="?attr/colorOutline"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLabelsTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🏷️ Labels:"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/labelsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/historyHeaderCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnToggleHistory"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="📜 Historial"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:padding="12dp"
|
||||
android:drawableEnd="@drawable/ic_expand_more"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/historyCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvHistory"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyHistory"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="No hay historial disponible"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressOptions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardExportPdf"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnExportPdf"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="📄 Exportar a PDF"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/save"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
598
app/src/main/res/layout/activity_spaces.xml
Normal file
598
app/src/main/res/layout/activity_spaces.xml
Normal file
@@ -0,0 +1,598 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
app:tabIndicatorColor="?attr/colorPrimary"
|
||||
app:tabMode="fixed"
|
||||
app:tabGravity="fill" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvOfflineBanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#FF9800"
|
||||
android:textColor="#000000"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:padding="12dp"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:alpha="0.3"
|
||||
android:visibility="gone" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/spacesContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeSpaces"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone"
|
||||
android:lineSpacingExtra="4dp" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/recentContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRecent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvRecentPages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyRecent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone"
|
||||
android:lineSpacingExtra="4dp" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressRecent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/sharesContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeShares"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvShares"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="16dp"
|
||||
android:paddingBottom="80dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyShares"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone"
|
||||
android:lineSpacingExtra="4dp" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressShares"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnNewShare"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/new_share_link"
|
||||
android:icon="@drawable/ic_add"
|
||||
style="@style/Widget.Material3.Button.TonalButton" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/settingsContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Profile -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:alpha="0.88"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/profile"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivAvatar"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
app:shapeAppearanceOverlay="@style/CircularImageView"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_page" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="16dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnChangeAvatar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/change_photo"
|
||||
style="@style/Widget.Material3.Button.TonalButton" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnRemoveAvatar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/remove_photo"
|
||||
android:visibility="gone"
|
||||
style="@style/Widget.Material3.Button.TextButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/name_hint"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etUserName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPersonName"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSaveProfile"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="@string/save_changes" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Appearance -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:alpha="0.88"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/appearance"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/background_image"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/background_image_description"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnChangeBackground"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/change"
|
||||
style="@style/Widget.Material3.Button.TonalButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnRemoveBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/remove_background_image"
|
||||
android:visibility="gone"
|
||||
style="@style/Widget.Material3.Button.TextButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Account -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:alpha="0.88"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/account"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmail"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnLogout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="@string/logout"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
app:strokeColor="?attr/colorError"
|
||||
android:textColor="?attr/colorError" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<include
|
||||
android:id="@+id/userCard"
|
||||
layout="@layout/item_user_card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnAddAccount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="@string/add_account"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Offline -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:alpha="0.88"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/offline_mode"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/offline_hint"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switchForcedOffline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/force_offline"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnCachedPages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="@string/cached_pages"
|
||||
style="@style/Widget.Material3.Button.TonalButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Deleted Pages -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:alpha="0.88"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/deleted_pages"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/deleted_pages_description"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDeletedPages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="@string/deleted_pages_button"
|
||||
style="@style/Widget.Material3.Button.TonalButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Biometric -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:alpha="0.88"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/biometric_lock"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switchBiometric"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/biometric_summary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabAddSpace"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginBottom="88dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/create_space"
|
||||
android:src="@drawable/ic_add"
|
||||
app:backgroundTint="?attr/colorPrimaryContainer"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabSearch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/search"
|
||||
android:src="@drawable/ic_search"
|
||||
app:backgroundTint="?attr/colorPrimaryContainer"
|
||||
app:tint="?attr/colorOnPrimaryContainer" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
79
app/src/main/res/layout/activity_version_viewer.xml
Normal file
79
app/src/main/res/layout/activity_version_viewer.xml
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/actionBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:background="?attr/colorSurface"
|
||||
android:elevation="2dp"
|
||||
android:gravity="center_vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivAvatar"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/circle_background" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAuthor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDateTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="11sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnLoadVersion"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Cargar esta versión"
|
||||
style="@style/Widget.Material3.Button.TonalButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webViewVersion"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressVersion"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
57
app/src/main/res/layout/dialog_add_account.xml
Normal file
57
app/src/main/res/layout/dialog_add_account.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/server_url_hint"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etServerUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/account_email_hint"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etAccountEmail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textEmailAddress"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/account_password_hint"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
app:passwordToggleEnabled="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etAccountPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
56
app/src/main/res/layout/dialog_context_menu.xml
Normal file
56
app/src/main/res/layout/dialog_context_menu.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Acciones"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnContextDuplicate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/duplicate"
|
||||
android:textSize="16sp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingHorizontal="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnContextMove"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/move_to_space"
|
||||
android:textSize="16sp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingHorizontal="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnContextMoveToPage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/move_to_page"
|
||||
android:textSize="16sp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingHorizontal="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnContextCopyLink"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/copy_link"
|
||||
android:textSize="16sp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingHorizontal="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
53
app/src/main/res/layout/dialog_create_page.xml
Normal file
53
app/src/main/res/layout/dialog_create_page.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/page_title"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilPageTitle"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:boxBackgroundMode="outline"
|
||||
app:boxStrokeColor="?attr/colorPrimary"
|
||||
app:boxStrokeWidth="2dp"
|
||||
app:boxCornerRadiusTopStart="8dp"
|
||||
app:boxCornerRadiusTopEnd="8dp"
|
||||
app:boxCornerRadiusBottomStart="8dp"
|
||||
app:boxCornerRadiusBottomEnd="8dp"
|
||||
app:hintEnabled="false"
|
||||
app:errorEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etPageTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textSize="16sp"
|
||||
android:hint="@string/page_title" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/offline_hint"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginTop="16dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
66
app/src/main/res/layout/dialog_create_space.xml
Normal file
66
app/src/main/res/layout/dialog_create_space.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivSpaceIcon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_page" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnChooseIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="@string/choose_icon"
|
||||
style="@style/Widget.Material3.Button.TonalButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/space_name"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etSpaceName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/space_description">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etSpaceDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="2"
|
||||
android:maxLines="4"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
34
app/src/main/res/layout/dialog_deleted_pages.xml
Normal file
34
app/src/main/res/layout/dialog_deleted_pages.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvDeletedPages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxHeight="400dp"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmptyDeleted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="No hay páginas eliminadas"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone"
|
||||
android:padding="32dp" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressDeleted"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
108
app/src/main/res/layout/dialog_edit_space.xml
Normal file
108
app/src/main/res/layout/dialog_edit_space.xml
Normal file
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivIconPreview"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="Icono del espacio"
|
||||
android:background="@drawable/ic_page"
|
||||
android:padding="4dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnChangeIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Cambiar icono"
|
||||
style="@style/Widget.Material3.Button.TextButton" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnRemoveIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Eliminar icono"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:textColor="?attr/colorError"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilSpaceName"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Nombre"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etSpaceName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilSpaceDescription"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Descripción">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etSpaceDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="3"
|
||||
android:gravity="top" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorOutlineVariant"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDeleteSpace"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:text="@string/delete_space"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
app:strokeColor="?attr/colorError"
|
||||
android:textColor="?attr/colorError" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
33
app/src/main/res/layout/dialog_emoji_picker.xml
Normal file
33
app/src/main/res/layout/dialog_emoji_picker.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/choose_emoji"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<GridView
|
||||
android:id="@+id/emojiGrid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:numColumns="6"
|
||||
android:horizontalSpacing="8dp"
|
||||
android:verticalSpacing="8dp"
|
||||
android:gravity="center"
|
||||
android:stretchMode="columnWidth" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnRemoveEmoji"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/remove_emoji"
|
||||
style="?android:attr/borderlessButtonStyle" />
|
||||
|
||||
</LinearLayout>
|
||||
38
app/src/main/res/layout/dialog_link_input.xml
Normal file
38
app/src/main/res/layout/dialog_link_input.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="URL del enlace">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etLinkUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:text="https://" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="Texto del enlace">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etLinkText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
58
app/src/main/res/layout/dialog_page_search.xml
Normal file
58
app/src/main/res/layout/dialog_page_search.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/select_target_page"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etSearchPage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:hint="@string/search_pages"
|
||||
android:inputType="text"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:background="@android:drawable/editbox_background"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnMoveToRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/move_to_root"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="italic"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvPageList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmptyPages"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_pages_found"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
41
app/src/main/res/layout/dialog_search.xml
Normal file
41
app/src/main/res/layout/dialog_search.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etSearch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:hint="@string/search"
|
||||
android:inputType="text"
|
||||
android:textSize="16sp"
|
||||
android:background="@drawable/bg_search"
|
||||
android:drawableStart="@drawable/ic_search"
|
||||
android:drawablePadding="12dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:maxLines="1"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSearchEmpty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone"
|
||||
android:lineSpacingExtra="4dp"
|
||||
android:padding="32dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvSearchResults"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
</LinearLayout>
|
||||
21
app/src/main/res/layout/dialog_space_list.xml
Normal file
21
app/src/main/res/layout/dialog_space_list.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/select_target_space"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvSpaceList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
5
app/src/main/res/layout/fragment_readonly_content.xml
Normal file
5
app/src/main/res/layout/fragment_readonly_content.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/webViewContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
25
app/src/main/res/layout/fragment_readonly_index.xml
Normal file
25
app/src/main/res/layout/fragment_readonly_index.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyIndex"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="No hay encabezados en esta página"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvIndex"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
250
app/src/main/res/layout/fragment_readonly_options.xml
Normal file
250
app/src/main/res/layout/fragment_readonly_options.xml
Normal file
@@ -0,0 +1,250 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/infoHeaderCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnToggleInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="ℹ️ Información"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:padding="12dp"
|
||||
android:drawableEnd="@drawable/ic_expand_more"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/infoCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Creado por:"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/creatorInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivCreator"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/circle_background" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCreator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
android:background="?attr/colorOutline" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Última actualización por:"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/updaterInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivUpdater"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/circle_background" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUpdater"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
android:background="?attr/colorOutline" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Fecha de creación:"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCreatedAt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="13sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUpdatedAt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="13sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/historyHeaderCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnToggleHistory"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="📜 Historial"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:padding="12dp"
|
||||
android:drawableEnd="@drawable/ic_expand_more"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/historyCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvHistory"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyHistory"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="No hay historial disponible"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/progressOptions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardExportPdf"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnExportPdf"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="📄 Exportar a PDF"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
31
app/src/main/res/layout/item_add_page.xml
Normal file
31
app/src/main/res/layout/item_add_page.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="56dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAddIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center"
|
||||
android:text="+"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?attr/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAddPage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/add_page"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
93
app/src/main/res/layout/item_comment.xml
Normal file
93
app/src/main/res/layout/item_comment.xml
Normal file
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivCommentAvatar"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_page" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCommentAuthor"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCommentTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="11sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCommentContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnEditComment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Editar"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:paddingEnd="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btnDeleteComment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Eliminar"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorError"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
53
app/src/main/res/layout/item_deleted_page.xml
Normal file
53
app/src/main/res/layout/item_deleted_page.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageIcon"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:gravity="center"
|
||||
android:textSize="24sp"
|
||||
android:text="📄" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDeletedAt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnRestore"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Restaurar"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
67
app/src/main/res/layout/item_history.xml
Normal file
67
app/src/main/res/layout/item_history.xml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/historyItem"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivAvatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@drawable/circle_background" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAuthor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDateTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btnViewVersion"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_visibility"
|
||||
android:contentDescription="Ver versión"
|
||||
android:padding="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:scaleType="centerInside" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorOutline" />
|
||||
|
||||
</LinearLayout>
|
||||
27
app/src/main/res/layout/item_index.xml
Normal file
27
app/src/main/res/layout/item_index.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingVertical="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvIndexIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvIndexText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
</LinearLayout>
|
||||
65
app/src/main/res/layout/item_page.xml
Normal file
65
app/src/main/res/layout/item_page.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/pageItemRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnExpandPage"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:src="@drawable/ic_expand_more"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Toggle children"
|
||||
android:scaleType="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivPageIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/ic_page"
|
||||
android:contentDescription="@string/page"
|
||||
app:tint="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnDeletePage"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/delete"
|
||||
android:scaleType="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
68
app/src/main/res/layout/item_recent_page.xml
Normal file
68
app/src/main/res/layout/item_recent_page.xml
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:alpha="0.88"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageIcon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:gravity="center"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivPageIcon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:src="@drawable/ic_page"
|
||||
android:scaleType="center"
|
||||
app:tint="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageDate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginTop="4dp"
|
||||
android:maxLines="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
79
app/src/main/res/layout/item_saved_account.xml
Normal file
79
app/src/main/res/layout/item_saved_account.xml
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:alpha="0.88"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/ivAccountAvatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
app:shapeAppearanceOverlay="@style/CircularImageView"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_page" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountEmail"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountUrl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:layout_marginTop="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSwitchAccount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:text="@string/switch_account"
|
||||
android:textSize="12sp"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_marginStart="8dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnRemoveAccount"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/remove_account"
|
||||
android:layout_marginStart="4dp"
|
||||
app:tint="?attr/colorError" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
65
app/src/main/res/layout/item_share.xml
Normal file
65
app/src/main/res/layout/item_share.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="12dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvShareIcon"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:gravity="center"
|
||||
android:text="🔗"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSharePageTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="15sp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvShareUrl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="11sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="middle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnCopyShareLink"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:src="@drawable/ic_link"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/copy_share_link"
|
||||
android:scaleType="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnDeleteShare"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/delete"
|
||||
android:scaleType="center" />
|
||||
|
||||
</LinearLayout>
|
||||
87
app/src/main/res/layout/item_share_card.xml
Normal file
87
app/src/main/res/layout/item_share_card.xml
Normal file
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:alpha="0.88"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvShareIcon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:gravity="center"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivShareIcon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:src="@drawable/ic_page"
|
||||
android:scaleType="center"
|
||||
app:tint="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSharePageTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvShareUrl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="11sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginTop="4dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="middle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnCopyShareLink"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:src="@drawable/ic_link"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/copy_share_link"
|
||||
android:scaleType="center" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnDeleteShare"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/delete"
|
||||
android:scaleType="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
157
app/src/main/res/layout/item_space.xml
Normal file
157
app/src/main/res/layout/item_space.xml
Normal file
@@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="2dp"
|
||||
app:strokeWidth="0dp"
|
||||
app:strokeColor="@android:color/transparent"
|
||||
style="@style/Widget.MaterialComponents.CardView"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivBlurredBg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/vScrim"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnExpand"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Expandir"
|
||||
android:src="@drawable/ic_expand_more"
|
||||
android:scaleType="center"
|
||||
android:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivSpaceIcon"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:contentDescription="@string/space_icon"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSpaceName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvRole"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.05"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:background="?attr/colorPrimaryContainer"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSpaceDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPageCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:visibility="gone"
|
||||
android:layout_marginEnd="4dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnEditSpace"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Editar espacio"
|
||||
android:src="@drawable/ic_pencil"
|
||||
android:scaleType="center"
|
||||
android:tint="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginStart="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pagesContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/ivGradientBorder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
27
app/src/main/res/layout/item_space_header.xml
Normal file
27
app/src/main/res/layout/item_space_header.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvHeaderLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:letterSpacing="0.04"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
55
app/src/main/res/layout/item_user_card.xml
Normal file
55
app/src/main/res/layout/item_user_card.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:alpha="0.88"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="?attr/colorOutlineVariant"
|
||||
app:cardBackgroundColor="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/headerUserCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/usuarios"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUserCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/accountList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
120
app/src/main/res/values/strings.xml
Normal file
120
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Docmost</string>
|
||||
<string name="app_subtitle">Cliente para Docmost Wiki</string>
|
||||
<string name="url_hint">URL del servidor (ej. https://wiki.ejemplo.com)</string>
|
||||
<string name="email_hint">Correo electrónico</string>
|
||||
<string name="password_hint">Contraseña</string>
|
||||
<string name="connect">Conectar</string>
|
||||
<string name="connecting">Conectando…</string>
|
||||
<string name="fill_all_fields">Completa todos los campos</string>
|
||||
<string name="no_spaces">No hay spaces disponibles</string>
|
||||
<string name="error_loading">Error al cargar datos</string>
|
||||
<string name="editing_page">Editando página</string>
|
||||
<string name="title_hint">Título</string>
|
||||
<string name="content_hint">Contenido (Markdown)</string>
|
||||
<string name="save">Guardar</string>
|
||||
<string name="saving">Guardando…</string>
|
||||
<string name="page_saved">Página guardada</string>
|
||||
<string name="logout">Cerrar sesión</string>
|
||||
<string name="logout_confirm">¿Estás seguro de cerrar sesión?</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="page">Página</string>
|
||||
<string name="search">Buscar</string>
|
||||
<string name="preview">Vista previa</string>
|
||||
<string name="edit">Editar</string>
|
||||
<string name="space_icon">Icono del espacio</string>
|
||||
<string name="add_emoji">➕</string>
|
||||
<string name="remove_emoji">Quitar emoji</string>
|
||||
<string name="choose_emoji">Elegir emoji</string>
|
||||
<string name="spaces_tab">Espacios</string>
|
||||
<string name="settings_tab">Ajustes</string>
|
||||
<string name="profile">Perfil</string>
|
||||
<string name="account">Cuenta</string>
|
||||
<string name="name_hint">Nombre</string>
|
||||
<string name="change_photo">Cambiar foto</string>
|
||||
<string name="remove_photo">Eliminar foto</string>
|
||||
<string name="save_changes">Guardar cambios</string>
|
||||
<string name="profile_updated">Perfil actualizado</string>
|
||||
<string name="create_space">Crear espacio</string>
|
||||
<string name="space_name">Nombre del espacio</string>
|
||||
<string name="space_slug">Slug del espacio</string>
|
||||
<string name="space_description">Descripción (opcional)</string>
|
||||
<string name="creating">Creando…</string>
|
||||
<string name="space_created">Espacio creado</string>
|
||||
<string name="choose_icon">Elegir icono</string>
|
||||
<string name="recent_tab">Recientes</string>
|
||||
<string name="no_recent_pages">No hay páginas recientes</string>
|
||||
<string name="add_page">+ Nueva página</string>
|
||||
<string name="page_title">Título de la página</string>
|
||||
<string name="create">Crear</string>
|
||||
<string name="delete">Eliminar</string>
|
||||
<string name="delete_confirm">¿Estás seguro de eliminar esta página?</string>
|
||||
<string name="deleting">Eliminando…</string>
|
||||
<string name="delete_space">Eliminar espacio</string>
|
||||
<string name="delete_space_confirm">¿Estás seguro de eliminar este espacio? Se eliminarán todas las páginas que contiene.</string>
|
||||
<string name="duplicate">Duplicar</string>
|
||||
<string name="move_to_space">Mover a otro espacio…</string>
|
||||
<string name="move_to_page">Mover a otra página…</string>
|
||||
<string name="select_target_page">Seleccionar página padre</string>
|
||||
<string name="search_pages">Buscar páginas…</string>
|
||||
<string name="no_pages_found">No se encontraron páginas</string>
|
||||
<string name="move_to_root">Mover a raíz del espacio</string>
|
||||
<string name="copy_link">Copiar enlace</string>
|
||||
<string name="link_copied">Enlace copiado al portapapeles</string>
|
||||
<string name="select_target_space">Seleccionar espacio de destino</string>
|
||||
<string name="page_duplicated">Página duplicada</string>
|
||||
<string name="page_moved">Página movida</string>
|
||||
<string name="offline_mode">Modo sin conexión</string>
|
||||
<string name="offline_banner">Viendo versión sin conexión</string>
|
||||
<string name="cached_pages">Páginas sin conexión</string>
|
||||
<string name="no_cached_pages">No hay páginas guardadas sin conexión</string>
|
||||
<string name="offline_hint">Las páginas que visites se guardan automáticamente</string>
|
||||
<string name="open_cached">Abrir sin conexión</string>
|
||||
<string name="force_offline">Forzar modo sin conexión</string>
|
||||
<string name="offline_not_available">No disponible sin conexión</string>
|
||||
<string name="shares_tab">Compartir</string>
|
||||
<string name="share_page">Compartir página</string>
|
||||
<string name="no_shares">No hay páginas compartidas</string>
|
||||
<string name="share_created">Enlace compartido creado</string>
|
||||
<string name="share_deleted">Enlace compartido eliminado</string>
|
||||
<string name="copy_share_link">Copiar enlace</string>
|
||||
<string name="select_page_to_share">Seleccionar página para compartir</string>
|
||||
<string name="search_page_to_share">Buscar página para compartir</string>
|
||||
<string name="confirm_share">¿Compartir esta página?</string>
|
||||
<string name="share_url_hint">Enlace público</string>
|
||||
<string name="new_share_link">Nuevo enlace compartido</string>
|
||||
<string name="biometric_lock">Bloqueo biométrico</string>
|
||||
<string name="biometric_summary">Requerir autenticación biométrica al abrir la app</string>
|
||||
<string name="biometric_title">Autenticación requerida</string>
|
||||
<string name="biometric_subtitle">Desbloquea la app para continuar</string>
|
||||
<string name="biometric_not_available">La autenticación biométrica no está disponible en este dispositivo</string>
|
||||
<string name="send">Enviar</string>
|
||||
<string name="update">Actualizar</string>
|
||||
<string name="no_comments">Sin comentarios</string>
|
||||
<string name="no_headings">Sin encabezados</string>
|
||||
<string name="write_comment">Escribir comentario…</string>
|
||||
<string name="quick_user_switch">Cambio rápido de usuario</string>
|
||||
<string name="add_account">+ Añadir cuenta</string>
|
||||
<string name="switch_account">Cambiar</string>
|
||||
<string name="current_account">Actual</string>
|
||||
<string name="remove_account">Eliminar cuenta</string>
|
||||
<string name="add_account_title">Añadir cuenta</string>
|
||||
<string name="server_url_hint">URL del servidor</string>
|
||||
<string name="account_email_hint">Email de la cuenta</string>
|
||||
<string name="account_password_hint">Contraseña</string>
|
||||
<string name="account_saved">Cuenta guardada</string>
|
||||
<string name="account_already_exists">Esta cuenta ya está guardada</string>
|
||||
<string name="account_removed">Cuenta eliminada</string>
|
||||
<string name="switched_to">Cambiado a %s</string>
|
||||
<string name="error_switching_account">Error al cambiar: %s</string>
|
||||
<string name="usuarios">Usuarios</string>
|
||||
<string name="appearance">Apariencia</string>
|
||||
<string name="background_image">Imagen de fondo</string>
|
||||
<string name="background_image_description">El blur se ajusta según el tema</string>
|
||||
<string name="change">Cambiar</string>
|
||||
<string name="remove_background_image">Quitar imagen de fondo</string>
|
||||
<string name="deleted_pages">Páginas eliminadas</string>
|
||||
<string name="deleted_pages_description">Ver y restaurar páginas eliminadas recientemente</string>
|
||||
<string name="deleted_pages_button">🗑️ Páginas eliminadas</string>
|
||||
</resources>
|
||||
16
app/src/main/res/values/themes.xml
Normal file
16
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Docmost" parent="Theme.Material3.DynamicColors.DayNight">
|
||||
<item name="android:statusBarColor">?attr/colorPrimary</item>
|
||||
<item name="android:navigationBarColor">?attr/colorSurface</item>
|
||||
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
|
||||
<item name="android:windowLightNavigationBar">?attr/isLightTheme</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowBackground">?attr/colorSurface</item>
|
||||
</style>
|
||||
|
||||
<style name="CircularImageView" parent="">
|
||||
<item name="cornerSize">50%</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
build.gradle.kts
Normal file
4
build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id("com.android.application") version "8.1.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
|
||||
}
|
||||
8
gradle.properties
Normal file
8
gradle.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
kotlin.code.style=official
|
||||
android.nonTransitiveRClass=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.configureondemand=true
|
||||
org.gradle.caching=true
|
||||
android.enableR8.fullMode=true
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
244
gradlew
vendored
Normal file
244
gradlew
vendored
Normal file
@@ -0,0 +1,244 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
16
settings.gradle.kts
Normal file
16
settings.gradle.kts
Normal file
@@ -0,0 +1,16 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Docmost"
|
||||
include(":app")
|
||||
Reference in New Issue
Block a user