diff --git a/README.md b/README.md index 414ade8..420c38f 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ UI library. * [json-editor](./src/json-editor/README.md): JSON editor. * [keyboard](./src/keyboard/README.md): Virtual keyboard. * [log](./src/log/README.md): Terminal log viewer. +* [logcat](./src/logcat/README.md): Android logcat viewer. * [markdown-editor](./src/markdown-editor/README.md): Markdown editor with preview. * [markdown-viewer](./src/markdown-viewer/README.md): Live markdown renderer. * [mask-editor](./src/mask-editor/README.md): Image mask editing. diff --git a/index.json b/index.json index 955af79..09821b8 100644 --- a/index.json +++ b/index.json @@ -162,12 +162,12 @@ "react": false }, "logcat": { + "react": true, "version": "0.1.0", "style": true, "icon": false, "test": true, "install": false, - "react": false, "dependencies": [] }, "lrc-player": { diff --git a/src/logcat/README.md b/src/logcat/README.md index 4432c9c..af4b996 100644 --- a/src/logcat/README.md +++ b/src/logcat/README.md @@ -25,3 +25,43 @@ npm install luna-logcat --save import 'luna-logcat/luna-logcat.css' import LunaLogcat from 'luna-logcat' ``` + +## Usage + +```javascript +const logcat = new LunaLogcat(container) +logcatp.append({ + date: '2021-01-01 00:00:00', + package: 'com.example', + pid: 1234, + tid: 1234, + priority: 3, + tag: 'tag', + message: 'message', +}) +``` + +## Configuration + +* entries(IEntry[]): Log entries. +* filter(IFilter): Log filter. +* maxNum(number): Max entry number, zero means infinite. +* wrapLongLines(boolean): Wrap long lines. + +## Api + +### append(entry: IEntry): void + +Append entry. + +### clear(): void + +Clear all entries. + +## Types + +### IFilter + +* package(string): Package name. +* priority(number): Entry priority. +* tag(string): Tag name. diff --git a/src/logcat/index.ts b/src/logcat/index.ts index 26c0065..0a71733 100644 --- a/src/logcat/index.ts +++ b/src/logcat/index.ts @@ -6,11 +6,16 @@ import throttle from 'licia/throttle' import isDate from 'licia/isDate' import each from 'licia/each' import strHash from 'licia/strHash' +import $ from 'licia/$' +import lowerCase from 'licia/lowerCase' +import contain from 'licia/contain' import dateFormat from 'licia/dateFormat' import { exportCjs } from '../share/util' /** IOptions */ export interface IOptions extends IComponentOptions { + /** Max entry number, zero means infinite. */ + maxNum?: number /** Log filter. */ filter?: IFilter /** Log entries. */ @@ -19,8 +24,14 @@ export interface IOptions extends IComponentOptions { wrapLongLines?: boolean } -interface IFilter { +/** IFilter */ +export interface IFilter { + /** Entry priority. */ priority?: number + /** Package name. */ + package?: string + /** Tag name. */ + tag?: string } interface IBaseEntry { @@ -52,6 +63,7 @@ interface IEntry extends IBaseEntry { * }) */ export default class Logcat extends Component { + private isAtBottom = true private render: types.AnyFn private entries: Array<{ container: HTMLElement @@ -61,6 +73,7 @@ export default class Logcat extends Component { super(container, { compName: 'logcat' }) this.initOptions(options, { + maxNum: 0, entries: [], wrapLongLines: false, }) @@ -78,8 +91,14 @@ export default class Logcat extends Component { this.bindEvent() } + destroy() { + this.$container.off('scroll', this.onScroll) + super.destroy() + } + /** Append entry. */ append(entry: IEntry) { - const { c } = this + const { c, entries } = this + const { maxNum } = this.options const date: Date = isDate(entry.date) ? (entry.date as Date) @@ -92,11 +111,18 @@ export default class Logcat extends Component { ...entry, date, } - this.entries.push({ + entries.push({ container, entry: e, }) + if (maxNum !== 0 && entries.length > maxNum) { + const entry = entries.shift() + if (entry) { + $(entry.container).remove() + } + } + const html = [ `${dateFormat( date, @@ -114,12 +140,23 @@ export default class Logcat extends Component { if (this.filterEntry(e)) { this.container.appendChild(container) + if (this.isAtBottom) { + this.scrollToBottom() + } } } + /** Clear all entries. */ clear() { this.entries = [] this.$container.html('') } + private scrollToBottom() { + const { container } = this + const { scrollHeight, scrollTop, offsetHeight } = container + if (scrollTop <= scrollHeight - offsetHeight) { + container.scrollTop = 10000000 + } + } private filterEntry(entry: IBaseEntry) { const { filter } = this.options @@ -131,12 +168,26 @@ export default class Logcat extends Component { return false } + if (filter.package) { + if (!contain(lowerCase(entry.package), lowerCase(filter.package))) { + return false + } + } + + if (filter.tag) { + if (!contain(lowerCase(entry.tag), lowerCase(filter.tag))) { + return false + } + } + return true } private bindEvent() { const { c } = this this.on('optionChange', (name, val) => { + const { entries } = this + switch (name) { case 'wrapLongLines': if (val) { @@ -145,21 +196,44 @@ export default class Logcat extends Component { this.$container.rmClass(c('wrap-long-lines')) } break + case 'maxNum': + if (val > 0 && entries.length > val) { + this.entries = entries.slice(entries.length - val) + this.render() + } + break case 'filter': this.render() break } }) + + this.$container.on('scroll', this.onScroll) + } + private onScroll = () => { + const { scrollHeight, offsetHeight, scrollTop } = this + .container as HTMLElement + + let isAtBottom = false + if (scrollHeight === offsetHeight) { + isAtBottom = true + } else if (Math.abs(scrollHeight - offsetHeight - scrollTop) < 1) { + isAtBottom = true + } + this.isAtBottom = isAtBottom } private _render() { const { container } = this this.$container.html('') + this.isAtBottom = true each(this.entries, (entry) => { if (this.filterEntry(entry.entry)) { container.appendChild(entry.container) } }) + + this.scrollToBottom() } } diff --git a/src/logcat/package.json b/src/logcat/package.json index 044551e..b84d6bf 100644 --- a/src/logcat/package.json +++ b/src/logcat/package.json @@ -1,5 +1,8 @@ { "name": "logcat", "version": "0.1.0", - "description": "Android logcat viewer" + "description": "Android logcat viewer", + "luna": { + "react": true + } } diff --git a/src/logcat/react.tsx b/src/logcat/react.tsx new file mode 100644 index 0000000..94ccfa7 --- /dev/null +++ b/src/logcat/react.tsx @@ -0,0 +1,46 @@ +import { CSSProperties, FC, useEffect, useRef } from 'react' +import each from 'licia/each' +import Logcat, { IOptions } from './index' +import { useNonInitialEffect } from '../share/hooks' + +interface IILogcatProps extends IOptions { + style?: CSSProperties + className?: string + onCreate?: (logcat: Logcat) => void +} + +const LunaLogcat: FC = (props) => { + const logcatRef = useRef(null) + const logcat = useRef() + + useEffect(() => { + const { maxNum, wrapLongLines, filter, entries } = props + logcat.current = new Logcat(logcatRef.current!, { + filter, + maxNum, + wrapLongLines, + entries, + }) + props.onCreate && props.onCreate(logcat.current) + + return () => logcat.current?.destroy() + }, []) + + each(['filter', 'maxNum', 'wrapLongLines'], (key: keyof IOptions) => { + useNonInitialEffect(() => { + if (logcat.current) { + logcat.current.setOption(key, props[key]) + } + }, [props[key]]) + }) + + return ( +
+ ) +} + +export default LunaLogcat diff --git a/src/logcat/story.js b/src/logcat/story.js index 7781377..397526c 100644 --- a/src/logcat/story.js +++ b/src/logcat/story.js @@ -4,7 +4,8 @@ import readme from './README.md' import story from '../share/story' import each from 'licia/each' import $ from 'licia/$' -import { boolean, number, select } from '@storybook/addon-knobs' +import { boolean, number, select, text } from '@storybook/addon-knobs' +import LunaLogcat from './react' import logs from './logcat.json' const def = story( @@ -12,10 +13,11 @@ const def = story( (container) => { $(container).css('height', '500px') - const { wrapLongLines, filter } = createKnobs() + const { wrapLongLines, maxNum, filter } = createKnobs() const logcat = new Logcat(container, { filter, + maxNum, wrapLongLines, }) each(logs, (log) => logcat.append(log)) @@ -25,6 +27,19 @@ const def = story( { readme, story: __STORY__, + ReactComponent() { + const { wrapLongLines, maxNum, filter } = createKnobs() + + return ( + each(logs, (log) => logcat.append(log))} + /> + ) + }, } ) @@ -40,15 +55,26 @@ function createKnobs() { }, 1 ) + const filterPackage = text('Filter Package', '') + const filterTag = text('Filter Tag', '') + const maxNum = number('Max Number', 500, { + range: true, + min: 10, + max: 1000, + step: 10, + }) const wrapLongLines = boolean('Wrap Long Lines', false) return { filter: { priority: filterPriority, + package: filterPackage, + tag: filterTag, }, + maxNum, wrapLongLines, } } export default def -export const { logcat } = def +export const { logcat: html, react } = def