diff --git a/.gitignore b/.gitignore index e6fab1b..de7c3d1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,17 @@ node_modules package-lock.json -# build -main.js +# yarn +yarn.lock + + *.js.map # obsidian data.json +#build_files +main.js + #vscode .vscode \ No newline at end of file diff --git a/.hotreload b/.hotreload new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..46563a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Zain Siddavatam and John Mavrick Reyes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9e4fe9c..43497e5 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,46 @@ -## Obsidian Sample Plugin +# Habitica Sync in Obsidian +This plugin for Obsidian incorporates a view to display and interact with the task management app Habitica. -This is a sample plugin for Obsidian (https://obsidian.md). +Please open issues for any bugs/functionality requests :) -This project uses Typescript to provide type checking and documentation. -The repo depends on the latest plugin API (obsidian.d.ts) in Typescript Definition format, which contains TSDoc comments describing what it does. +## Usage +The plugin's view is enabled by clicking on the "Open Habitica Pane" option in the side ribbon (default hotkey is `Ctrl+Shift+H`). -**Note:** The Obsidian API is still in early alpha and is subject to change at any time! +To sync your Habitica account, go to the settings page of the plugin and enter your user ID and API token credentials. +## Features +### Pane View +#### View stats (HP, XP, coins) +#### Views +Task Information: +- Title, description, subtasks + - Markdown and emoji support -This sample plugin demonstrates some of the basic functionality the plugin API can do. -- Changes the default font color to red using `styles.css`. -- Adds a ribbon icon, which shows a Notice when clicked. -- Adds a command "Open Sample Modal" which opens a Modal. -- Adds a plugin setting tab to the settings page. -- Registers a global click event and output 'click' to the console. -- Registers a global interval which logs 'setInterval' to the console. +Tabs: +- To Do's + - Active/Completed +- Dailies + - Due/Not Due/Completed + - [![Image from Gyazo](https://i.gyazo.com/1966b17f954dcffa954922570e860a06.png)](https://gyazo.com/1966b17f954dcffa954922570e860a06) +- Habits + - [![Image from Gyazo](https://i.gyazo.com/280494e620fc91548838d5b29a62652b.png)](https://gyazo.com/280494e620fc91548838d5b29a62652b) +- Rewards +#### Interactivity +- Check off tasks/dailies in the view + - Can uncheck completed habits/todos + - [![Image from Gyazo](https://i.gyazo.com/efb858cd9d54f9d9df936da1bd5858ed.gif)](https://gyazo.com/efb858cd9d54f9d9df936da1bd5858ed) +- modify habit counters (+/-) -### First time developing plugins? +### Settings -Quick starting guide for new plugin devs: +The following two inputs help fetch your user data to be displayed in the Obsidian view: +- **Habitica User ID:** You can find this by clicking on the "User" icon in the top right of the Habitica webapp, "Settings", then "API" +- **Habitica Token API:** You can find this by clicking on the "User" icon in the top right of the Habitica webapp, "Settings", then "API" +- **Show Task Descriptions:** Toggles whether description/notes for tasks will be shown or not +- **Show Subtasks:** Toggles whether subtasks for to do's/dailies will be shown or not -- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it). -- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder. -- Install NodeJS, then run `npm i` in the command line under your repo folder. -- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`. -- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`. -- Reload Obsidian to load the new version of your plugin. -- Enable plugin in settings window. -- For updates to the Obsidian API run `npm update` in the command line under your repo folder. +## Roadmap -### Releasing new releases +*Feel free to support us and donate!* -- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release. -- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible. -- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases -- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. -- Publish the release. +Buy Me a Coffee at ko-fi.com -### Adding your plugin to the community plugin list - -- Publish an initial version. -- Make sure you have a `README.md` file in the root of your repo. -- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin. - -### How to use - -- Clone this repo. -- `npm i` or `yarn` to install dependencies -- `npm run dev` to start compilation in watch mode. - -### Manually installing the plugin - -- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`. - -### API Documentation - -See https://github.com/obsidianmd/obsidian-api diff --git a/ReactView.tsx b/ReactView.tsx deleted file mode 100644 index e924dac..0000000 --- a/ReactView.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from "react"; -import App from "./view/App"; - -export const ReactView = () => { - return ; -}; \ No newline at end of file diff --git a/main.ts b/main.ts deleted file mode 100644 index f701dd2..0000000 --- a/main.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Plugin } from "obsidian"; -import { ExampleSettingsTab } from "./settings"; -import { ExampleView, VIEW_TYPE_EXAMPLE} from "./view" - -interface ExamplePluginSettings { - dateFormat: string -} -const DEFAULT_SETTINGS: Partial = { - dateFormat: "YYYY-MM-DD" -} -export default class ExamplePlugin extends Plugin { - settings: ExamplePluginSettings; - view: ExampleView; - - async onload() { - await this.loadSettings(); - this.addSettingTab(new ExampleSettingsTab(this.app, this)); - this.registerView( - VIEW_TYPE_EXAMPLE, - (leaf) => (this.view = new ExampleView(leaf)) - ); - this.addRibbonIcon("dice", "Activate view", () => { //activate view - this.activateView(); - }); - } - async loadSettings() { - this.settings = Object.assign(DEFAULT_SETTINGS, await this.loadData()) - } - async saveSettings() { - await this.saveData(this.settings); - } - async onunload() { - await this.view.onClose(); - - this.app.workspace - .getLeavesOfType(VIEW_TYPE_EXAMPLE) - .forEach((leaf) => leaf.detach()); - } - async activateView() { - this.app.workspace.detachLeavesOfType(VIEW_TYPE_EXAMPLE); - - await this.app.workspace.getRightLeaf(false).setViewState({ - type: VIEW_TYPE_EXAMPLE, - active: true, - }); - - this.app.workspace.revealLeaf( - this.app.workspace.getLeavesOfType(VIEW_TYPE_EXAMPLE)[0] - ); - } - -} diff --git a/manifest.json b/manifest.json index 4915397..2ae5bfe 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,11 @@ { - "id": "test-plugin", - "name": "Test Plugin", - "version": "0.0.1", + "id": "obsidian-habitica-integration", + "name": "Habitica Sync", + "version": "1.0.2", "minAppVersion": "0.9.12", - "description": "This is a sample plugin for Obsidian. This plugin demonstrates some of the capabilities of the Obsidian API.", - "author": "Leoh", + "description": "This plugin helps integrate Habitica user tasks and stats into Obsidian", + "author": "Leoh and Ran", "authorUrl": "", - "isDesktopOnly": false + "isDesktopOnly": false, + "js": "main.js" } diff --git a/package.json b/package.json index 27eb3d7..37371e6 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,51 @@ { - "name": "test-plugin", - "version": "0.12.0", - "description": "This is a sample plugin for Obsidian (https://obsidian.md)", + "name": "obsidian-habitica-integration", + "version": "1.0.0", + "description": "This plugin allows for Habitica integration into Obsidian", "main": "main.js", "scripts": { - "dev": "rollup --config rollup.config.js -w", - "build": "rollup --config rollup.config.js --environment BUILD:production" + "dev": "rollup --config rollup.config.mjs -w", + "build": "rollup --config rollup.config.mjs --environment BUILD:production", + "dev2": "obsidian-plugin dev src/main.ts" }, "keywords": [], - "author": "", + "author": "Leonard and Ran", "license": "MIT", "devDependencies": { - "@rollup/plugin-commonjs": "^18.0.0", - "@rollup/plugin-node-resolve": "^11.2.1", - "@rollup/plugin-typescript": "^8.2.1", - "@types/node": "^14.14.37", - "@types/react": "^17.0.27", - "@types/react-dom": "^17.0.9", - "css-loader": "^6.4.0", - "extract-text-webpack-plugin": "^2.1.2", - "obsidian": "^0.12.0", - "rollup": "^2.32.1", - "style-loader": "^3.3.0", - "tslib": "^2.2.0", - "typescript": "^4.2.4" + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-typescript": "^11.1.6", + "@types/markdown-it": "^13.0.7", + "@types/markdown-it-emoji": "^2.0.4", + "@types/node": "^20.11.0", + "@types/node-emoji": "^1.8.2", + "@types/react": "^18.2.47", + "@types/react-dom": "^18.2.18", + "@types/react-tabs": "^5.0.4", + "@types/twemoji": "^13.1.1", + "css-loader": "^6.9.0", + "mini-css-extract-plugin": "^2.7.7", + "obsidian": "^1.4.11", + "obsidian-plugin-cli": "^0.0.5", + "rollup": "^4.9.4", + "style-loader": "^3.3.4", + "tslib": "^2.6.2", + "typescript": "^5.3.3", + "webpack": "^5.89.0" }, "dependencies": { - "node": "^16.10.0", - "node-fetch": "^3.0.0", - "react": "^17.0.2", - "react-dom": "^17.0.2" + "markdown-it": "^14.0.0", + "markdown-it-emoji": "^2.0.2", + "moment": "^2.30.1", + "node": "^21.2.0", + "node-emoji": "^2.1.3", + "node-fetch": "^3.3.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-emoji-render": "^2.0.1", + "react-markdown": "^9.0.1", + "react-tabs": "^6.0.2", + "twemoji": "^14.0.2" } } diff --git a/rollup.config.js b/rollup.config.mjs similarity index 89% rename from rollup.config.js rename to rollup.config.mjs index dd4d041..1551460 100644 --- a/rollup.config.js +++ b/rollup.config.mjs @@ -1,30 +1,32 @@ -import typescript from '@rollup/plugin-typescript'; -import {nodeResolve} from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; - -const isProd = (process.env.BUILD === 'production'); - -const banner = -`/* -THIS IS A GENERATED/BUNDLED FILE BY ROLLUP -if you want to view the source visit the plugins github repository -*/ -`; - -export default { - input: 'main.ts', - output: { - dir: '.', - sourcemap: 'inline', - sourcemapExcludeSources: isProd, - format: 'cjs', - exports: 'default', - banner, - }, - external: ['obsidian'], - plugins: [ - typescript(), - nodeResolve({browser: true}), - commonjs(), - ] +import typescript from '@rollup/plugin-typescript'; +import {nodeResolve} from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; + +const isProd = (process.env.BUILD === 'production'); + +const banner = +`/* +THIS IS A GENERATED/BUNDLED FILE BY ROLLUP +if you want to view the source visit the plugins github repository +*/ +`; + +export default { + input: 'src/main.ts', + output: { + dir: '.', + sourcemap: 'inline', + sourcemapExcludeSources: isProd, + format: 'cjs', + exports: 'default', + banner, + }, + external: ['obsidian'], + plugins: [ + typescript(), + nodeResolve({browser: true}), + commonjs(), + json(), + ] }; \ No newline at end of file diff --git a/settings.ts b/settings.ts deleted file mode 100644 index 8d40205..0000000 --- a/settings.ts +++ /dev/null @@ -1,30 +0,0 @@ -import ExamplePlugin from "main"; -import { App, PluginSettingTab, Setting } from "obsidian"; - -export class ExampleSettingsTab extends PluginSettingTab { - plugin: ExamplePlugin; - - constructor(app: App, plugin: ExamplePlugin) { - super(app, plugin) - this.plugin = plugin - } - - display(): void { - let { containerEl } = this; - containerEl.empty(); - - new Setting(containerEl) - .setName("Date format") - .setDesc("Default date format") - .addText((text) => - - text - .setPlaceholder("MMMM dd, yyyy") - .setValue(this.plugin.settings.dateFormat) - .onChange(async (value) => { - this.plugin.settings.dateFormat = value; - await this.plugin.saveSettings(); - }) - ); - } -} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..b8eaff4 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,71 @@ +import { Plugin } from "obsidian"; +import { HabiticaSyncSettingsTab } from "./settings"; +import { HabiticaSyncView, VIEW_TYPE} from "./view" + +interface HabiticaSyncSettings { + userID: string + apiToken: string + showTaskDescription: boolean + showSubTasks: boolean + dueDateFormat: string +} +const DEFAULT_SETTINGS: Partial = { + userID: "", + apiToken: "", + showTaskDescription: true, + showSubTasks: true, + dueDateFormat: "DD-MM-YYYY" +} +export default class HabiticaSync extends Plugin { + settings: HabiticaSyncSettings; + view: HabiticaSyncView; + + async onload() { + console.log("load plugin: habitica-sync") + await this.loadSettings(); + this.addSettingTab(new HabiticaSyncSettingsTab(this.app, this)); + this.registerView( + VIEW_TYPE, + (leaf) => (new HabiticaSyncView(leaf, this)) + ); + this.addRibbonIcon("popup-open", "Open Habitica Pane", () => { + this.activateView(); + }); + this.addCommand({ + id: "habitica-view-open", + name: "Open Pane", + hotkeys: [{ modifiers: ["Mod", "Shift"], key: "h"}], + callback: () => { + this.activateView(); + } + }); + + } + async loadSettings() { + this.settings = Object.assign(DEFAULT_SETTINGS, await this.loadData()) + } + async saveSettings() { + await this.saveData(this.settings); + } + + async onunload() { + await this.view.onClose(); + + this.app.workspace + .getLeavesOfType(VIEW_TYPE) + .forEach((leaf) => leaf.detach()); + } + async activateView() { + this.app.workspace.detachLeavesOfType(VIEW_TYPE); + + await this.app.workspace.getRightLeaf(false).setViewState({ + type: VIEW_TYPE, + active: true, + }); + + this.app.workspace.revealLeaf( + this.app.workspace.getLeavesOfType(VIEW_TYPE)[0] + ); + } + +} diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..974fe44 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,80 @@ +import HabiticaSync from "./main"; +import { App, PluginSettingTab, Setting } from "obsidian"; +import moment from "moment"; + +export class HabiticaSyncSettingsTab extends PluginSettingTab { + plugin: HabiticaSync; + + constructor(app: App, plugin: HabiticaSync) { + super(app, plugin) + this.plugin = plugin + } + + display(): void { + let { containerEl } = this; + containerEl.empty(); + + new Setting(containerEl) + .setName("Habitica User ID") + .setDesc("Can be found in Settings > API") + .addText((text) => + text + .setPlaceholder("User ID") + .setValue(this.plugin.settings.userID) + .onChange(async (value) => { + this.plugin.settings.userID = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName("Habitica API Token") + .setDesc("Can be found in Settings > API") + .addText((text) => + text + .setPlaceholder("API Token") + .setValue(this.plugin.settings.apiToken) + .onChange(async (value) => { + this.plugin.settings.apiToken = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) + .setName("Show Task Descriptions") + .setDesc("Updates require pane re-opening") + .addToggle(cb => { + cb + .setValue(this.plugin.settings.showTaskDescription) + .onChange(async (isEnable) => { + this.plugin.settings.showTaskDescription = isEnable; + await this.plugin.saveSettings(); + }) + }); + + new Setting(containerEl) + .setName("Show Sub-Tasks") + .setDesc("Updates require pane re-opening") + .addToggle(cb => { + cb + .setValue(this.plugin.settings.showSubTasks) + .onChange(async (isEnable) => { + this.plugin.settings.showSubTasks = isEnable; + await this.plugin.saveSettings(); + }) + }); + new Setting(containerEl) + .setName("Due Date Format") + .setDesc("Update requires pane re-opening, check moment.js docs for formatting. Current Format: " + moment().format(this.plugin.settings.dueDateFormat)) + .addText((text) => + text + .setPlaceholder("DD-MM-YYYY") + .setValue(this.plugin.settings.dueDateFormat) + .onChange(async (value) => { + this.plugin.settings.dueDateFormat = value; + await this.plugin.saveSettings(); + }) + ); + + } +} \ No newline at end of file diff --git a/src/view.tsx b/src/view.tsx new file mode 100644 index 0000000..be66720 --- /dev/null +++ b/src/view.tsx @@ -0,0 +1,37 @@ +import { ItemView,WorkspaceLeaf } from "obsidian"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import App from "./view/App" +import HabiticaSync from "./main"; + + +export const VIEW_TYPE = "example-view" + +export class HabiticaSyncView extends ItemView { + plugin: HabiticaSync; + constructor(leaf: WorkspaceLeaf, plugin: HabiticaSync) { + super(leaf) + this.plugin = plugin + } + + getViewType() { + return VIEW_TYPE + } + + getDisplayText() { + return "Habitica Pane" + } + getIcon(): string { + return "popup-open" + } + + async onOpen() { + ReactDOM.render( + , + this.containerEl.children[1] + ) + } + async onClose(){ + ReactDOM.unmountComponentAtNode(this.containerEl.children[1]); + } +} \ No newline at end of file diff --git a/src/view/App.tsx b/src/view/App.tsx new file mode 100644 index 0000000..1406d23 --- /dev/null +++ b/src/view/App.tsx @@ -0,0 +1,227 @@ +import * as React from "react"; +import { Notice } from "obsidian"; +import { getStats, scoreTask, makeCronReq, costReward, scoreChecklistItem } from "./habiticaAPI" +import Statsview from "./Components/Statsview" +import Taskview from "./Components/Taskview" +import ReactDOM from "react-dom"; + +class App extends React.Component { + private _username = ""; + public get username() { + return this._username; + } + public set username(value) { + this._username = value; + } + private _credentials = ""; + public get credentials() { + return this._credentials; + } + public set credentials(value) { + this._credentials = value; + } + constructor(props: any) { + super(props) + this.username = this.props.plugin.settings.userID + this.credentials = this.props.plugin.settings.apiToken + this.state = { + needCron: false, + isLoaded: false, + user_data: { + profile: { + name: "", + }, + stats: { + hp: 0, + lvl: 0, + gold: 0, + }, + lastCron: "", + }, + todos: [], + dailys: [], + habits: [], + } + this.handleChangeTodos = this.handleChangeTodos.bind(this); + this.handleChangeDailys = this.handleChangeDailys.bind(this); + this.handleChangeHabits = this.handleChangeHabits.bind(this); + this.handleChangeRewards = this.handleChangeRewards.bind(this); + this.handleChangeChecklistItem = this.handleChangeChecklistItem.bind(this); + this.runCron = this.runCron.bind(this); + + } + CheckCron(lastCron: string) { + let cronDate = new Date(lastCron); + let now = new Date(); + if (cronDate.getDate() != now.getDate() || (cronDate.getMonth() != now.getMonth() || cronDate.getFullYear() != now.getFullYear())) { + return ( +
+
Welcome back! Please check your tasks for the last day and hit continue to get your daily rewards.
+ +
+ ); + } + else { + return null + }; + } + async runCron() { + console.log("running cron"); + try { + let response = await makeCronReq(this.username, this.credentials); + this.setState({ + needCron: false, + }) + } catch (error) { + console.log(error); + new Notice("There was an error running the cron. Please try again later."); + } + this.reloadData(); + } + async reloadData() { + try { + let response = await getStats(this.username, this.credentials); + let result = await response.json(); + if (result.success === false) { + new Notice('Login Failed, Please check credentials and try again!'); + } + else { + this.setState({ + isLoaded: true, + user_data: result, + tasks: result.tasks, + }); + } + } catch (e) { + console.log(e); + new Notice("API Error: Please check credentials") + } + } + componentDidMount() { + this.reloadData() + } + + async sendScore(id: string, score: string, message: string) { + try { + let response = await scoreTask(this.username, this.credentials, id, score); + let result = await response.json(); + if (result.success === true) { + new Notice(message); + this.reloadData(); + } else { + new Notice("Resyncing, please try again"); + this.reloadData(); + } + } catch (e) { + console.log(e); + new Notice("API Error: Please check credentials") + } + } + + async sendReward(id: string, score: string, message: string) { + try { + let response = await costReward(this.username, this.credentials, id, score); + let result = await response.json(); + if (result.success === true) { + new Notice(message); + this.reloadData(); + } else { + new Notice("Resyncing, please try again"); + this.reloadData(); + } + } catch (e) { + console.log(e); + new Notice("API Error: Please check credentials") + } + } + + handleChangeTodos(event: any) { + this.state.tasks.todos.forEach((element: any) => { + if (element.id == event.target.id) { + if (!element.completed) { + this.sendScore(event.target.id, "up", "Checked!") + } else { + this.sendScore(event.target.id, "down", "Un-Checked!") + } + } + }) + } + handleChangeDailys(event: any) { + this.state.tasks.dailys.forEach((element: any) => { + if (element.id == event.target.id) { + if (element.id == event.target.id) { + if (!element.completed) { + this.sendScore(event.target.id, "up", "Checked!") + } else { + this.sendScore(event.target.id, "down", "Un-Checked!") + } + } + } + }) + } + handleChangeHabits(event: any) { + const target_id = event.target.id.slice(4) + if (event.target.id.slice(0, 4) == "plus") { + this.state.tasks.habits.forEach((element: any) => { + if (element.id == target_id) { + this.sendScore(target_id, "up", "Plus!") + } + }) + } + else { + this.state.tasks.habits.forEach((element: any) => { + if (element.id == target_id) { + this.sendScore(target_id, "down", "Minus :(") + } + }) + } + } + handleChangeRewards(event: any) { + const target_id = event.target.id + this.state.tasks.rewards.forEach((element: any) => { + if (element.id == event.target.id) { + if (element.id == target_id) { + this.sendReward(target_id, "down", "Redeemed!") + } + } + }) + } + async handleChangeChecklistItem(event: any){ + let parentID = event.target.parentNode.parentNode.parentNode.getAttribute("id") + let targetID = event.target.id + console.log(parentID+ " , " + targetID) + try{ + let response = await scoreChecklistItem(this.username, this.credentials, targetID, parentID); + let result = await response.json(); + if (result.success === true) { + new Notice("Checked!"); + this.reloadData(); + } else { + new Notice("Resyncing, please try again"); + this.reloadData(); + } + } catch (e) { + console.log(e); + new Notice("API Error: Please check credentials") + } + } + + render() { + let content = this.CheckCron(this.state.user_data.lastCron); + if (this.state.error) + return (
Loading....
) + else if (!this.state.isLoaded) + return
Loading....
+ else { + return (
+ {content} + + + + +
+ ); + } + } +} +export default App \ No newline at end of file diff --git a/src/view/Components/Statsview/index.css b/src/view/Components/Statsview/index.css new file mode 100644 index 0000000..e69de29 diff --git a/src/view/Components/Statsview/index.tsx b/src/view/Components/Statsview/index.tsx new file mode 100644 index 0000000..506cae4 --- /dev/null +++ b/src/view/Components/Statsview/index.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +export default function Index(props: any) { + return( +
+ {/*
{props.user_data.profile.name}
*/} +
HP: {numberWithCommas((props.user_data.stats.hp).toFixed(0))}
+
LEVEL: {props.user_data.stats.lvl}
+
GOLD: {numberWithCommas(props.user_data.stats.gp.toFixed(2))}
+
+ ); +} +function numberWithCommas(x: any) { + return x.toString().replace(/\B(? + +
+

+
+ {/* {console.log(props.checklist)} */} + +
+ + + ) +} + +export default DailyItem \ No newline at end of file diff --git a/src/view/Components/Taskview/Dailiesview/DailySubTasks.tsx b/src/view/Components/Taskview/Dailiesview/DailySubTasks.tsx new file mode 100644 index 0000000..09e4c99 --- /dev/null +++ b/src/view/Components/Taskview/Dailiesview/DailySubTasks.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import renderMarkdown from "../markdownRender"; + +function DailySubTasks(props: any) { + + if (props.subtasks) { + const subtasks = props.subtasks.map((subtask: any) => { + let subtask_text = renderMarkdown(subtask.text); + return ( +
+ +

+
+ ) + }); + return subtasks + } + else { + return
+ } +} +export default DailySubTasks \ No newline at end of file diff --git a/src/view/Components/Taskview/Dailiesview/index.tsx b/src/view/Components/Taskview/Dailiesview/index.tsx new file mode 100644 index 0000000..db5944b --- /dev/null +++ b/src/view/Components/Taskview/Dailiesview/index.tsx @@ -0,0 +1,107 @@ +import * as React from "react"; +import DailyItem from "./DailyItem" +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; + +export default function Index(props: any){ + + if(props.dailys == undefined) { + return
No Dailies Present
+ } + else { + + const notDueDailies = props.dailys.map((daily: any) => { + + if (!daily.isDue) { + let daily_notes = ''; + let daily_subtasks = ''; + if (props.settings.showTaskDescription) { + daily_notes = daily.notes; + } + + if (props.settings.showSubTasks) { + daily_subtasks = daily.checklist; + } + return + } + }) + + const incompleteDailies = props.dailys.map((daily: any) => { + if (!daily.completed&&daily.isDue) { + let daily_notes = ''; + let daily_subtasks = ''; + if (props.settings.showTaskDescription) { + daily_notes = daily.notes; + } + + if (props.settings.showSubTasks) { + daily_subtasks = daily.checklist; + } + return + } + }) + const completedDailies = props.dailys.map((daily: any) => { + // if(daily.completed) + // return + if (daily.completed) { + let daily_notes = ''; + let daily_subtasks = ''; + if (props.settings.showTaskDescription) { + daily_notes = daily.notes; + } + + if (props.settings.showSubTasks) { + daily_subtasks = daily.checklist; + } + return + } + }) + + const allDailies = props.dailys.map((daily: any) => { + // if(daily.completed) + // return + let daily_notes = ''; + let daily_subtasks = ''; + if (props.settings.showTaskDescription) { + daily_notes = daily.notes; + } + + if (props.settings.showSubTasks) { + daily_subtasks = daily.checklist; + } + return + }) + + const display =
+ + + Active + Completed + Not Due + All + + +
    {incompleteDailies}
+
+ +
    {completedDailies}
+
+ +
    {notDueDailies}
+
+ +
    {allDailies}
+
+
+
+ return(display); + } + +} diff --git a/src/view/Components/Taskview/Habitsview/HabitItem.tsx b/src/view/Components/Taskview/Habitsview/HabitItem.tsx new file mode 100644 index 0000000..37d672c --- /dev/null +++ b/src/view/Components/Taskview/Habitsview/HabitItem.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import renderMarkdown from "../markdownRender"; + +function HabitItem(props: any) { + let habit_text = renderMarkdown(props.habit_text); + let habit_notes = renderMarkdown(props.habit_notes); + return ( +
+
+ + +
+
+

+
+
+
+ ) +} + +export default HabitItem \ No newline at end of file diff --git a/src/view/Components/Taskview/Habitsview/index.tsx b/src/view/Components/Taskview/Habitsview/index.tsx new file mode 100644 index 0000000..1d0107c --- /dev/null +++ b/src/view/Components/Taskview/Habitsview/index.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import HabitItem from "./HabitItem" + +export default function Index(props: any){ + if(props.habits == undefined) { + return (
+ No habits present. +
) + } + else { + const allHabits = props.habits.map((habit: any) => { + if (props.settings.showTaskDescription) { + return + } else { + return + } + }) + const display =
+
    {allHabits}
+
+ + return(display); + } +} + diff --git a/src/view/Components/Taskview/Rewardview/RewardItem.tsx b/src/view/Components/Taskview/Rewardview/RewardItem.tsx new file mode 100644 index 0000000..5f331e2 --- /dev/null +++ b/src/view/Components/Taskview/Rewardview/RewardItem.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import renderMarkdown from "../markdownRender"; + +function RewardItem(props: any) { + let reward_text = renderMarkdown(props.reward_text); + let reward_notes = renderMarkdown(props.reward_notes); + return ( +
+
+ +
+
+

+
+
+ +
+ ) +} + +export default RewardItem \ No newline at end of file diff --git a/src/view/Components/Taskview/Rewardview/index.tsx b/src/view/Components/Taskview/Rewardview/index.tsx new file mode 100644 index 0000000..4ddc21a --- /dev/null +++ b/src/view/Components/Taskview/Rewardview/index.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import RewardItem from "./RewardItem" + +export default function Index(props: any){ + if(props.rewards == undefined) { + return (
+ No Rewards present. +
) + } + else { + const allRewards = props.rewards.map((reward: any) => { + if (props.settings.showTaskDescription) { + return + } else { + return + } + }) + const display =
+
    {allRewards}
+
+ + return(display); + } +} + diff --git a/src/view/Components/Taskview/Todoview/TodoItem.tsx b/src/view/Components/Taskview/Todoview/TodoItem.tsx new file mode 100644 index 0000000..7c1653b --- /dev/null +++ b/src/view/Components/Taskview/Todoview/TodoItem.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import TodoSubTasks from "./TodoSubTasks"; +import renderMarkdown from "../markdownRender" +import moment from "moment"; + +function TodoItem(props: any) { + var dueDate = (props.dueDate==null)?"":("Due Date:"+(moment(props.dueDate).format(props.dueDateFormat))); + var text_html = renderMarkdown(props.todo_text); + var note_html = renderMarkdown(props.todo_notes); + return ( +
+ +
+

+
+ +
{dueDate}
+
+
+ ) +} + +export default TodoItem \ No newline at end of file diff --git a/src/view/Components/Taskview/Todoview/TodoSubTasks.tsx b/src/view/Components/Taskview/Todoview/TodoSubTasks.tsx new file mode 100644 index 0000000..92ab97a --- /dev/null +++ b/src/view/Components/Taskview/Todoview/TodoSubTasks.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import renderMarkdown from "../markdownRender"; + +function TodoSubTasks(props: any) { + if (props.subtasks) { + const subtasks = props.subtasks.map((subtask: any) => { + let subtask_text = renderMarkdown(subtask.text); + return ( +
+ +

+
+ ) + }); + return subtasks + } + else { + return
+ } +} +export default TodoSubTasks \ No newline at end of file diff --git a/src/view/Components/Taskview/Todoview/index.tsx b/src/view/Components/Taskview/Todoview/index.tsx new file mode 100644 index 0000000..035b52e --- /dev/null +++ b/src/view/Components/Taskview/Todoview/index.tsx @@ -0,0 +1,50 @@ +import * as React from "react"; +import TodoItem from "./TodoItem" +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; + +export default function Index(props: any){ + if(props.todos == undefined) { + return
No Todos present.
+ } + else { + const incompleteTodos = props.todos.map((todo: any) => { + + if(!todo.completed) { + let todo_notes = ''; + let todo_subtasks = ''; + if (props.settings.showTaskDescription) { + todo_notes = todo.notes; + } + + if (props.settings.showSubTasks) { + todo_subtasks = todo.checklist; + } + return + } + + }) + const completedTodos = props.todos.map((todo: any) => { + if(todo.completed) + return + }) + const display =
+ + + Active + Completed + + +
    {incompleteTodos}
+
+ +
    {completedTodos}
+
+
+
+ + + return(display); + } +} \ No newline at end of file diff --git a/src/view/Components/Taskview/index.tsx b/src/view/Components/Taskview/index.tsx new file mode 100644 index 0000000..06d2874 --- /dev/null +++ b/src/view/Components/Taskview/index.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; +import Dailiesview from "./Dailiesview" +import Habitsview from "./Habitsview" +import Todoview from "./Todoview" +import Rewardview from "./Rewardview" +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; + +export default function Index(props: any){ + const display =
+ + + + + today + + + add_chart + + + assignment_turned_in + + + account_balance + + + + + + + + + + + + + + + +
+ return(display); +} + diff --git a/src/view/Components/Taskview/markdownRender.ts b/src/view/Components/Taskview/markdownRender.ts new file mode 100644 index 0000000..e09aa1f --- /dev/null +++ b/src/view/Components/Taskview/markdownRender.ts @@ -0,0 +1,21 @@ +import MarkdownIt from "markdown-it"; +import markdownitEmoji from "markdown-it-emoji" +import twemoji from "twemoji"; + +export default function renderMarkdown(markdown: string) { + //check if markdown is empty or not a string + if (markdown === "" || markdown === undefined) { + return ""; + } + const md = new MarkdownIt({ + html: true, + breaks: true, + linkify: true, + typographer: true + }); + md.use(markdownitEmoji); + md.renderer.rules.emoji = function(token, idx) { + return twemoji.parse(token[idx].content); + }; + return md.render(markdown); +} \ No newline at end of file diff --git a/src/view/habiticaAPI.ts b/src/view/habiticaAPI.ts new file mode 100644 index 0000000..eae3ab8 --- /dev/null +++ b/src/view/habiticaAPI.ts @@ -0,0 +1,70 @@ +// import fetch from "node-fetch"; + +export async function getStats(username: string, credentials: string){ + const url = "https://habitica.com/export/userdata.json" + const response = await fetch(url, { + method: 'GET', + headers: { + "Content-Type": "application/json", + "x-client": "278e719e-5f9c-43b1-9dba-8b73343dc062-HabiticaSync", + "x-api-user": username, + "x-api-key": credentials, + }, + }) + return (await response) +} + +export async function scoreTask(username: string, credentials: string, taskID: string, direction: string) { + const url = "https://habitica.com/api/v3/tasks/".concat(taskID).concat("/score/").concat(direction) + const response = fetch(url, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "x-client": "278e719e-5f9c-43b1-9dba-8b73343dc062-HabiticaSync", + "x-api-user": username, + "x-api-key": credentials, + } + }) + return(response) +} +export async function makeCronReq(username: string, credentials: string){ + const url = "https://habitica.com/api/v3/cron"; + const response = fetch(url, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "x-client": "278e719e-5f9c-43b1-9dba-8b73343dc062-HabiticaSync", + "x-api-user": username, + "x-api-key": credentials, + } + }) + return(response) +} + +export async function costReward(username: string, credentials: string, taskID: string, direction: string) { + const url = "https://habitica.com/api/v4/tasks/".concat(taskID).concat("/score/").concat(direction) + const response = fetch(url, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "x-client": "278e719e-5f9c-43b1-9dba-8b73343dc062-HabiticaSync", + "x-api-user": username, + "x-api-key": credentials, + } + }) + return(response) +} + +export async function scoreChecklistItem(username: string, credentials: string, checklistItemID: string, taskID: string) { + const url = "https://habitica.com/api/v3/tasks/".concat(taskID).concat("/checklist/").concat(checklistItemID).concat("/score") + const response = fetch(url, { + method: 'POST', + headers: { + "Content-Type": "application/json", + "x-client": "278e719e-5f9c-43b1-9dba-8b73343dc062-HabiticaSync", + "x-api-user": username, + "x-api-key": credentials, + } + }) + return(response) +} \ No newline at end of file diff --git a/styles.css b/styles.css index 7312bb1..90c9ec1 100644 --- a/styles.css +++ b/styles.css @@ -1,36 +1,323 @@ -/* Empty. change later */ - -.book { - border: 1px solid var(--background-modifier-border); - padding: 10px; - } - - .book__title { - font-weight: 600; - } - - .book__author { - color: var(--text-muted); - } - -.todo-item { - display: flex; - justify-content: flex-start; - align-items: center; - padding: 30px 20px 0; - width: 70%; - border-bottom: 1px solid #cecece; - font-family: Roboto, sans-serif; - font-weight: 100; - font-size: 15px; - color: #ffffff; -} - -input[type=checkbox] { - margin-right: 10px; - font-size: 30px; -} - -input[type=checkbox]:focus { - outline: 0; -} \ No newline at end of file +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@1,300&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Open Sans:ital,wght@0,400;1,100&family=Roboto&display=swap'); + +.add-task-input { + display: flex; +} + +#profile-name { + font-size: x-large; + font-weight: bold; + padding-bottom: 3%; +} +.stats { + width: 95%; + display: flex; + justify-content: space-between; + font-weight: bold; + padding-bottom: 5px; +} + +.stats-view { + border-bottom: 1px; +} +.modify-todo { + align-self: center; +} + +.delete-todo { + align-self: center; +} + +.todo-item { + display: grid; + grid-template-columns: 1fr 30fr 1fr 1fr; + justify-content: left; + align-items: flex-start; + padding-top: 5px; + padding-bottom: 5px; + width: 100%; + border-bottom: 1px solid #cecece; + font-family: Roboto, sans-serif; + font-weight: bold; + font-size: 16px; + padding-left: 0; +} + +.description { + font-family: Open Sans, sans-serif; + font-weight: 100; +} +.description > ul { + list-style-type: none; +} +p { + margin: 0; +} + +.habit-text { + text-align: left !important; + font-weight: bold; + padding-top: 5px; + width: 80%; + margin-right: 20px; +} + +.habit-button { + background-color: var(--interactive-accent); + border: none; + /* color: black; */ + text-align: center; + text-decoration: none; + font-size: 16px; + display: block; + width: 100%; + color: var(--text-on-accent); +} +/* habit-button on hover css selector */ +.habit-button:hover { + color: var(--text-on-accent); + background-color: var(--interactive-accent-hover); +} + +.habit-item { + display: flex; + grid-template-columns: 60px 1fr; + width: 100%; + gap: 5px; + border-bottom: 1px solid #cecece; + font-family: Open Sans, sans-serif; + font-weight: 100%; + font-size: 16px; + padding-top: 5px; + padding-bottom: 5px; +} + +.habit-button-grp { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-content: stretch; + height: 100%; +} + +input[type=checkbox] { + margin-right: 10px; + margin-top: 5px; + align-self: start; +} + +.todo-content { + align-self: center; +} + +.submit-button { + /* padding: 5px 5px; */ + font-size: 15px; + border: 1px solid #aaa; + /* white-space: nowrap; */ + /* margin: 10px; */ + margin: 0; +} + +input[type=checkbox]:focus { + outline: 0; +} + +::-webkit-scrollbar { + display: none; /* Chrome Safari */ +} + +.plugin-root { + min-width: 260px; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; +} + +#classDisplay { + height: 100%; + width: 100%; +} + + +.view-content { + margin-bottom: 0; +} + +.substats { + font-size: medium; +} + +ul { + margin: 0; +} + +/* react-tabs internal file :wink: */ +.material-icons { + font-size: 12px !important; + padding-top: 1px; + padding-right: 2px; + +} + +.material-icons.md-18 { font-size: 18px !important; } +.material-icons.md-24 { font-size: 24px !important; } +.material-icons.md-32 { font-size: 32px !important; } +.material-icons.md-48 { font-size: 48px !important; } + + +.react-tabs { + -webkit-tap-highlight-color: transparent; + height: 100%; +} + +.react-tabs__tab-list { + border-bottom: 1px solid #aaa; + margin: 0 0 5px; + padding: 0; +} + +.react-tabs__tab { + display: inline-block; + border: 1px solid transparent; + border-bottom: none; + bottom: -1px; + position: relative; + list-style: none; + padding: 1% 2%; + cursor: pointer; + font-size: medium; +} + +.react-tabs__tab--selected { + background: var(--interactive-accent); + color: white; + border-radius: 5px 5px 0 0; +} + +.react-tabs__tab--disabled { + color: GrayText; + cursor: default; +} + +.react-tabs__tab:focus { + box-shadow: 0 0 5px hsl(208, 99%, 50%); + border-color: hsl(208, 99%, 50%); + outline: none; +} + +.react-tabs__tab:focus:after { + content: ""; + position: absolute; + height: 5px; + left: -4px; + right: -4px; + bottom: -5px; + background: #fff; +} + +.react-tabs__tab-panel { + display: none; + left: 0px; + height: 88%; + /* overflow: scroll; */ +} + +.task-panel { + overflow: scroll; + height: 100%; +} + +.react-tabs__tab-panel--selected { + display: block; +} +ul li:not(.task-list-item)::before { + content: "•"; + color: transparent; + display: inline-block; + width: 1em; + margin-left: -1em; + padding: 0; + font-weight: bold; + text-shadow: 0 0 0.5em transparent; +} + +.task-operation { + align-self: center; + margin: 0; + padding: 0; +} + +.edit-item { + display: flex; + flex-direction: column; + width: 100%; +} + +.edit-button { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.task-submit { + display: grid; + grid-template-columns: 10fr 5fr; +} + +.add-task-input { + display: block; + width: 100%; +} + +.task-input-box { + margin-right: 10px; +} + +button { + margin: auto; + margin-bottom: 5px; + white-space: nowrap; + padding: 5px 5px; + margin-right: 0; +} +.cron { + display: inline-grid; + justify-content: center; + text-align: center;; + margin-top: 5px; + margin-left: 10%; + margin-right: 10%; + margin-bottom: 10px; + border-radius: 10px; +} +#cronMessage { + margin: 20px; + margin-bottom: 10px; + color: var(--text-normal) +} +#cronButton { + margin: auto; + margin-bottom: 5px; + white-space: nowrap; + padding: 5px 5px; + background-color: var(--interactive-accent); + margin-right: auto; +} + +.subtask { + display: flex; + flex-direction: row; + font-weight: normal; + font-family: Roboto, sans-serif; + justify-content: flex-start; +} +.emoji { + height: 1em; +} +.description>ul { + list-style-type: disc; + margin-left: 10% !important; +} diff --git a/tsconfig.json b/tsconfig.json index 4968d9a..c6bbfcb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,13 +3,14 @@ "baseUrl": ".", "inlineSourceMap": true, "inlineSources": true, - "module": "esnext", + "module": "ESNext", + "target": "es6", "jsx": "react", - "target": "es2017", - "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", "importHelpers": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, "lib": [ "dom", "es5", @@ -20,4 +21,4 @@ "include": [ "**/*.ts" ] -} +} \ No newline at end of file diff --git a/view.ts b/view.ts deleted file mode 100644 index d54ef70..0000000 --- a/view.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ItemView,WorkspaceLeaf } from "obsidian"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import { ReactView } from "./ReactView"; - - -export const VIEW_TYPE_EXAMPLE = "example-view" - -export class ExampleView extends ItemView { - constructor(leaf: WorkspaceLeaf) { - super(leaf) - } - - getViewType() { - return VIEW_TYPE_EXAMPLE - } - - getDisplayText() { - return "Example View" - } - - async onOpen() { - ReactDOM.render( - React.createElement(ReactView), - this.containerEl.children[1] - ) - } - - async onClose(){ - ReactDOM.unmountComponentAtNode(this.containerEl.children[1]); - } -} \ No newline at end of file diff --git a/view/App.tsx b/view/App.tsx deleted file mode 100644 index a723428..0000000 --- a/view/App.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from "react"; -import { getTasks } from "./habiticaAPI" -import TodoItem from "./TodoItem" - - -const username = "5b70c4ea-ed91-4fc4-8231-edd1984ec02c" -const credentials = "ccbeec3b-fe55-4952-a2fa-023d0fbbab85" - -class App extends React.Component { - constructor(props: any) { - super(props) - this.state = { - isLoaded: false, - tasks: "" - } - } - componentDidMount() { - getTasks(username, credentials) - .then(res => res.json()) - .then( - result => { - console.log(result.data) - this.setState({ - isLoaded: true, - tasks: result.data - }) - }, - (error) => { - this.setState({ - isLoaded: true, - error - }) - } - ) - - } - render(){ - const { error, isLoaded, tasks } = this.state; - if (error) { - return
Error: {error.message}
; - } else if (!isLoaded) { - return
Loading...
; - } else { - const listItems = tasks.map((tasks: any) => - - - ); - return ( -
    {listItems}
- ); - } - } -} -export default App \ No newline at end of file diff --git a/view/TodoItem.tsx b/view/TodoItem.tsx deleted file mode 100644 index 84a5415..0000000 --- a/view/TodoItem.tsx +++ /dev/null @@ -1,12 +0,0 @@ - import * as React from "react"; - -function TodoItem(props: any) { - return ( -
- -

{props.task.text}

-
- ) -} - -export default TodoItem \ No newline at end of file diff --git a/view/habiticaAPI.ts b/view/habiticaAPI.ts deleted file mode 100644 index 39b9eaf..0000000 --- a/view/habiticaAPI.ts +++ /dev/null @@ -1,16 +0,0 @@ -// import fetch from "node-fetch"; - - -export async function getTasks(username: string, credentials: string){ - const url = "https://habitica.com/api/v3/tasks/user?type=todos" - const response = fetch(url, { - method: 'GET', - headers: { - "Content-Type": "application/json", - "x-client": username.concat("-testAPI"), - "x-api-user": username, - "x-api-key": credentials, - }, - }) - return (response) -}