Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,77 @@ npm start
http://127.0.0.1:48912
```

### Method 3: Android/Termux Installation

1. Install Termux from [F-Droid](https://f-droid.org/packages/com.termux/) (Google Play version is outdated).

2. Install the required packages:

```bash
pkg update && pkg upgrade
pkg install nodejs-lts git binutils build-essential python libvips imagemagick
```

> **What these packages are for:**
> - `nodejs-lts` — Node.js runtime
> - `git` — clone the repository
> - `binutils`, `build-essential` — C++ compiler toolchain (for building native modules)
> - `python` — required by `node-gyp` (used by `better-sqlite3` and `sharp`)
> - `libvips` — image processing library (required by `sharp`)
> - `imagemagick` — additional image format support (required by `libvips`)

3. Apply the gyp fix (prevents native module build errors):

```bash
mkdir -p ~/.gyp && echo "{ 'variables': { 'android_ndk_path': '' } }" > ~/.gyp/include.gypi
```

4. Clone the repository:

```bash
git clone https://github.com/dmitryplyaskin/SillyInnkeeper.git
cd SillyInnkeeper
```

5. Install server dependencies:

```bash
cd server
npm install
```

6. Install client dependencies:

```bash
cd ../client
npm install
```

7. Build the client:

```bash
cd ../client
npm run build
```

8. Start the server:

```bash
cd ../server
npm start
```

9. Open your browser and navigate to:

```
http://127.0.0.1:48912
```

> ⚠️ **Note on Android limitations**:
> - The folder picker icon is hidden — type the full path manually (e.g., `/storage/emulated/0/Download/characters`).
> - The "Open in Explorer" button and context menu item in card details are also hidden.
> - These features are disabled because system GUI dialogs are not available from a server/browser context on Android/Termux.

### Configuration (.env)

You can configure **host/port** and SillyTavern integration via a root `.env` file.
Expand Down
4 changes: 2 additions & 2 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@mantine/core": "8.3.10",
"@mantine/core": "^8.3.10",
"@mantine/form": "^8.3.10",
"@mantine/hooks": "8.3.10",
"@mantine/hooks": "^8.3.10",
"@mantine/notifications": "^8.3.10",
"@tabler/icons-react": "^3.36.0",
"dompurify": "^3.3.1",
Expand Down
29 changes: 16 additions & 13 deletions client/src/entities/cards/ui/card-actions-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function CardActionsMenu({

const exportPngUrl = `/api/cards/${encodeURIComponent(cardId)}/export.png?download=1`;
const canUseFile = Boolean(filePath?.trim());
const isAndroid = /android/i.test(navigator.userAgent);

return (
<Menu withinPortal position="bottom-end" shadow="md">
Expand Down Expand Up @@ -122,19 +123,21 @@ export function CardActionsMenu({

<Menu.Divider />

<Menu.Item
leftSection={
isOpeningInExplorer ? <Loader size={16} /> : <IconFolder size={16} />
}
disabled={!canUseFile || isOpeningInExplorer}
onClick={(e) => {
e.stopPropagation();
if (!filePath) return;
onOpenInExplorer({ filePath });
}}
>
{t("cardDetails.openInExplorer")}
</Menu.Item>
{!isAndroid && (
<Menu.Item
leftSection={
isOpeningInExplorer ? <Loader size={16} /> : <IconFolder size={16} />
}
disabled={!canUseFile || isOpeningInExplorer}
onClick={(e) => {
e.stopPropagation();
if (!filePath) return;
onOpenInExplorer({ filePath });
}}
>
{t("cardDetails.openInExplorer")}
</Menu.Item>
)}

<Menu.Item
leftSection={<IconPencil size={16} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export function CardDetailsActionsPanel({
$settings,
]);

const isAndroid = /android/i.test(navigator.userAgent);

function canonicalizeForCompare(value: unknown): unknown {
if (value === null || value === undefined) return null;
if (Array.isArray(value)) return value.map(canonicalizeForCompare);
Expand Down Expand Up @@ -281,6 +283,7 @@ export function CardDetailsActionsPanel({
const fp = (p ?? "").trim();
if (!fp) return;
if (openingDuplicatePath) return;
if (isAndroid) return;
setOpeningDuplicatePath(fp);
try {
await showFile(fp);
Expand Down Expand Up @@ -419,20 +422,22 @@ export function CardDetailsActionsPanel({
{i18n.t("cardDetails.download")}
</Button>

<Button
fullWidth
variant="subtle"
color="gray"
onClick={() => {
const p = (details?.file_path ?? "").trim();
if (!p) return;
onOpenInExplorer({ filePath: p });
}}
disabled={!details?.file_path}
loading={isOpeningInExplorer}
>
{i18n.t("cardDetails.openInExplorer")}
</Button>
{!isAndroid && (
<Button
fullWidth
variant="subtle"
color="gray"
onClick={() => {
const p = (details?.file_path ?? "").trim();
if (!p) return;
onOpenInExplorer({ filePath: p });
}}
disabled={!details?.file_path}
loading={isOpeningInExplorer}
>
{i18n.t("cardDetails.openInExplorer")}
</Button>
)}
<Button
fullWidth
variant="subtle"
Expand Down Expand Up @@ -549,6 +554,7 @@ export function CardDetailsActionsPanel({
setSelectedDuplicatePath(p);
setConfirmDeleteDuplicateOpened(true);
}}
isAndroid={isAndroid}
/>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ export function CardDuplicatesSection({
onMakeMain,
onOpenInExplorer,
onRequestDelete,
isAndroid = false,
}: {
duplicates: string[];
isSettingMainFile: boolean;
openingDuplicatePath: string | null;
onMakeMain: (filePath: string) => void;
onOpenInExplorer: (filePath: string) => void;
onRequestDelete: (filePath: string) => void;
isAndroid?: boolean;
}) {
if (!duplicates || duplicates.length === 0) return null;

Expand Down Expand Up @@ -50,16 +52,18 @@ export function CardDuplicatesSection({
</ActionIcon>
</Tooltip>

<Tooltip label={i18n.t("cardDetails.showInExplorer")} withArrow>
<ActionIcon
variant="light"
onClick={() => onOpenInExplorer(p)}
loading={openingDuplicatePath === p}
aria-label={i18n.t("cardDetails.showInExplorer")}
>
<IconFolder size={18} />
</ActionIcon>
</Tooltip>
{!isAndroid && (
<Tooltip label={i18n.t("cardDetails.showInExplorer")} withArrow>
<ActionIcon
variant="light"
onClick={() => onOpenInExplorer(p)}
loading={openingDuplicatePath === p}
aria-label={i18n.t("cardDetails.showInExplorer")}
>
<IconFolder size={18} />
</ActionIcon>
</Tooltip>
)}

<Tooltip label={i18n.t("cardDetails.deleteDuplicate")} withArrow>
<ActionIcon
Expand Down
43 changes: 25 additions & 18 deletions client/src/features/cards-import/ui/cards-import-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function CardsImportModal() {

const isMove = settings.importMode === "move";
const canImport = Boolean(settings.sourceFolderPath?.trim());
const isAndroid = /android/i.test(navigator.userAgent);
const setImportMode = (v: string) => {
if (v === "copy" || v === "move") onChangeImportMode(v);
};
Expand Down Expand Up @@ -108,28 +109,34 @@ export function CardsImportModal() {

<TextInput
label={t("cardsImport.sourceFolderLabel")}
placeholder={t("cardsImport.sourceFolderPlaceholder")}
placeholder={
isAndroid
? t("settingsForm.androidPlaceholder")
: t("cardsImport.sourceFolderPlaceholder")
}
value={settings.sourceFolderPath ?? ""}
onChange={(e) => onChangePath(e.currentTarget.value)}
rightSectionWidth={42}
rightSectionWidth={isAndroid ? 0 : 42}
rightSection={
<Tooltip
label={t("cardsImport.pickFolderTooltip")}
withArrow
position="top"
>
<ActionIcon
variant="subtle"
onClick={() => {
if (isLoading) return;
onPick();
}}
disabled={isLoading}
aria-label={t("cardsImport.pickFolderAria")}
isAndroid ? null : (
<Tooltip
label={t("cardsImport.pickFolderTooltip")}
withArrow
position="top"
>
{isPickingFolder ? <Loader size={16} /> : <FolderIcon />}
</ActionIcon>
</Tooltip>
<ActionIcon
variant="subtle"
onClick={() => {
if (isLoading) return;
onPick();
}}
disabled={isLoading}
aria-label={t("cardsImport.pickFolderAria")}
>
{isPickingFolder ? <Loader size={16} /> : <FolderIcon />}
</ActionIcon>
</Tooltip>
)
}
/>

Expand Down
Loading