Pages (#4811)
* wip * wip * wip * Update page-editor.vue * wip * wip * wip * wip * wip * wip * wip * Update page-editor.variable.core.vue * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update aiscript.ts * wip * Update package.json * wip * wip * wip * wip * wip * Update page.vue * wip * wip * wip * wip * more info * wip fn * wip * wip * wip
This commit is contained in:
		
							parent
							
								
									747a0b1791
								
							
						
					
					
						commit
						05b8111c19
					
				
					 52 changed files with 3583 additions and 37 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -20,3 +20,4 @@ api-docs.json
 | 
				
			||||||
yarn.lock
 | 
					yarn.lock
 | 
				
			||||||
.DS_Store
 | 
					.DS_Store
 | 
				
			||||||
/files
 | 
					/files
 | 
				
			||||||
 | 
					ormconfig.json
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										26
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										26
									
								
								CHANGELOG.md
									
										
									
									
									
								
							| 
						 | 
					@ -42,6 +42,32 @@ mongodb:
 | 
				
			||||||
8. master ブランチに戻す
 | 
					8. master ブランチに戻す
 | 
				
			||||||
9. enjoy
 | 
					9. enjoy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					unreleased
 | 
				
			||||||
 | 
					-------------------
 | 
				
			||||||
 | 
					### New features
 | 
				
			||||||
 | 
					#### MisskeyPages
 | 
				
			||||||
 | 
					ページ(記事)を作成できるように。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* 後から何度でも編集できる
 | 
				
			||||||
 | 
					* アイキャッチを設定できる
 | 
				
			||||||
 | 
					* フォントを設定できる
 | 
				
			||||||
 | 
					* 画像を好きな位置に挿入できる
 | 
				
			||||||
 | 
					* URLを決められる
 | 
				
			||||||
 | 
					* タイトルを設定できる
 | 
				
			||||||
 | 
					* 見出しを設定できる
 | 
				
			||||||
 | 
					* ページの要約を設定できる(URLプレビュー時などに便利)
 | 
				
			||||||
 | 
					* 変数や式(aka AiScript)を使用して動的なページも作れる
 | 
				
			||||||
 | 
					* 目次自動生成(coming soon)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ページを気に入ったら「いいね」しよう (coming soon)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Improvements
 | 
				
			||||||
 | 
					* APIコンソールでパラメータテンプレートを表示するように
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixes
 | 
				
			||||||
 | 
					* おすすめユーザーに自分自身が含まれる問題を修正
 | 
				
			||||||
 | 
					* ユーザーサジェストで表示名が変わらない問題を修正
 | 
				
			||||||
 | 
					
 | 
				
			||||||
11.4.0 (2019/04/25)
 | 
					11.4.0 (2019/04/25)
 | 
				
			||||||
-------------------
 | 
					-------------------
 | 
				
			||||||
### Improvements
 | 
					### Improvements
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,6 +65,7 @@ common:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  trash: "ゴミ箱"
 | 
					  trash: "ゴミ箱"
 | 
				
			||||||
  drive: "ドライブ"
 | 
					  drive: "ドライブ"
 | 
				
			||||||
 | 
					  pages: "ページ"
 | 
				
			||||||
  messaging: "トーク"
 | 
					  messaging: "トーク"
 | 
				
			||||||
  home: "ホーム"
 | 
					  home: "ホーム"
 | 
				
			||||||
  deck: "デッキ"
 | 
					  deck: "デッキ"
 | 
				
			||||||
| 
						 | 
					@ -1813,26 +1814,6 @@ docs:
 | 
				
			||||||
  edit-this-page-on-github: "間違いや改善点を見つけましたか?"
 | 
					  edit-this-page-on-github: "間違いや改善点を見つけましたか?"
 | 
				
			||||||
  edit-this-page-on-github-link: "このページをGitHubで編集"
 | 
					  edit-this-page-on-github-link: "このページをGitHubで編集"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  api:
 | 
					 | 
				
			||||||
    entities:
 | 
					 | 
				
			||||||
      properties: "プロパティ"
 | 
					 | 
				
			||||||
    endpoints:
 | 
					 | 
				
			||||||
      params: "パラメータ"
 | 
					 | 
				
			||||||
      no-params: "パラメータはありません"
 | 
					 | 
				
			||||||
      res: "レスポンス"
 | 
					 | 
				
			||||||
      require-credential: "このエンドポイントは認証情報が必須です。"
 | 
					 | 
				
			||||||
      require-permission: "このエンドポイントは{permission}の権限を必要とします。"
 | 
					 | 
				
			||||||
      has-limit: "レートリミットがあります。"
 | 
					 | 
				
			||||||
      duration-limit: "直近{duration}ミリ秒の間のこのエンドポイントへのリクエスト数の合計が{max}を超える場合はリクエストできません。"
 | 
					 | 
				
			||||||
      min-interval-limit: "前回のリクエストから{interval}ミリ秒経っていない場合はリクエストできません。"
 | 
					 | 
				
			||||||
      show-src: "このエンドポイントのソースコードも閲覧できます。"
 | 
					 | 
				
			||||||
      show-src-link: "コードをGitHubで見る"
 | 
					 | 
				
			||||||
      generated: "このドキュメントはAPI定義に基づき自動生成されています。"
 | 
					 | 
				
			||||||
    props:
 | 
					 | 
				
			||||||
      name: "名前"
 | 
					 | 
				
			||||||
      type: "型"
 | 
					 | 
				
			||||||
      description: "説明"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
dev/views/index.vue:
 | 
					dev/views/index.vue:
 | 
				
			||||||
  manage-apps: "アプリの管理"
 | 
					  manage-apps: "アプリの管理"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1857,3 +1838,165 @@ dev/views/new-app.vue:
 | 
				
			||||||
  authority: "権限"
 | 
					  authority: "権限"
 | 
				
			||||||
  authority-desc: "ここで要求した機能だけがAPIからアクセスできます。"
 | 
					  authority-desc: "ここで要求した機能だけがAPIからアクセスできます。"
 | 
				
			||||||
  authority-warning: "アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。"
 | 
					  authority-warning: "アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pages:
 | 
				
			||||||
 | 
					  new-page: "ページの作成"
 | 
				
			||||||
 | 
					  edit-page: "ページの編集"
 | 
				
			||||||
 | 
					  page-created: "ページを作成しました"
 | 
				
			||||||
 | 
					  page-updated: "ページを更新しました"
 | 
				
			||||||
 | 
					  are-you-sure-delete: "このページを削除しますか?"
 | 
				
			||||||
 | 
					  page-deleted: "ページを削除しました"
 | 
				
			||||||
 | 
					  edit-this-page: "このページを編集"
 | 
				
			||||||
 | 
					  variables: "変数"
 | 
				
			||||||
 | 
					  variables-info: "変数を使うことで動的なページを作成できます。テキスト内で <b>{ 変数名 }</b> と書くとそこに変数の値を埋め込めます。例えば <b>Hello { thing } world!</b> というテキストで、変数(thing)の値が <b>ai</b> だった場合、テキストは <b>Hello ai world!</b> になります。"
 | 
				
			||||||
 | 
					  variables-info2: "変数の評価(値を算出すること)は上から下に行われるので、ある変数の中で自分より下の変数を参照することはできません。例えば上から <b>A、B、C</b> と3つの変数を定義したとき、<b>C</b>の中で<b>A</b>や<b>B</b>を参照することはできますが、<b>A</b>の中で<b>B</b>や<b>C</b>を参照することはできません。"
 | 
				
			||||||
 | 
					  variables-info3: "ユーザーからの入力を受け取るには、ページに「ユーザー入力」ブロックを設置し、「変数名」に入力を格納したい変数名を設定します(変数は自動で作成されます)。その変数を使ってユーザー入力に応じた動作を行えます。"
 | 
				
			||||||
 | 
					  more-details: "詳しい説明"
 | 
				
			||||||
 | 
					  title: "タイトル"
 | 
				
			||||||
 | 
					  url: "ページURL"
 | 
				
			||||||
 | 
					  summary: "ページの要約"
 | 
				
			||||||
 | 
					  align-center: "中央寄せ"
 | 
				
			||||||
 | 
					  font: "フォント"
 | 
				
			||||||
 | 
					  fontSerif: "セリフ"
 | 
				
			||||||
 | 
					  fontSansSerif: "サンセリフ"
 | 
				
			||||||
 | 
					  set-eye-catchig-image: "アイキャッチ画像を設定"
 | 
				
			||||||
 | 
					  remove-eye-catchig-image: "アイキャッチ画像を削除"
 | 
				
			||||||
 | 
					  choose-block: "ブロックを追加"
 | 
				
			||||||
 | 
					  select-type: "種類を選択"
 | 
				
			||||||
 | 
					  enter-variable-name: "変数名を決めてください"
 | 
				
			||||||
 | 
					  the-variable-name-is-already-used: "その変数名は既に使われています"
 | 
				
			||||||
 | 
					  blocks:
 | 
				
			||||||
 | 
					    text: "テキスト"
 | 
				
			||||||
 | 
					    section: "セクション"
 | 
				
			||||||
 | 
					    image: "画像"
 | 
				
			||||||
 | 
					    button: "ボタン"
 | 
				
			||||||
 | 
					    input: "ユーザー入力"
 | 
				
			||||||
 | 
					    _input:
 | 
				
			||||||
 | 
					      name: "変数名"
 | 
				
			||||||
 | 
					      text: "タイトル"
 | 
				
			||||||
 | 
					      default: "デフォルト値"
 | 
				
			||||||
 | 
					      inputType: "入力の種類"
 | 
				
			||||||
 | 
					      _inputType:
 | 
				
			||||||
 | 
					        string: "テキスト"
 | 
				
			||||||
 | 
					        number: "数値"
 | 
				
			||||||
 | 
					    switch: "スイッチ"
 | 
				
			||||||
 | 
					    _switch:
 | 
				
			||||||
 | 
					      name: "変数名"
 | 
				
			||||||
 | 
					      text: "タイトル"
 | 
				
			||||||
 | 
					      default: "デフォルト値"
 | 
				
			||||||
 | 
					    _button:
 | 
				
			||||||
 | 
					      text: "タイトル"
 | 
				
			||||||
 | 
					      action: "ボタンを押したときの動作"
 | 
				
			||||||
 | 
					      _action:
 | 
				
			||||||
 | 
					        dialog: "ダイアログを表示する"
 | 
				
			||||||
 | 
					        _dialog:
 | 
				
			||||||
 | 
					          content: "内容"
 | 
				
			||||||
 | 
					        resetRandom: "乱数をリセット"
 | 
				
			||||||
 | 
					  script:
 | 
				
			||||||
 | 
					    categories:
 | 
				
			||||||
 | 
					      flow: "制御"
 | 
				
			||||||
 | 
					      logical: "論理演算"
 | 
				
			||||||
 | 
					      operation: "計算"
 | 
				
			||||||
 | 
					      comparison: "比較"
 | 
				
			||||||
 | 
					      random: "ランダム"
 | 
				
			||||||
 | 
					      value: "値"
 | 
				
			||||||
 | 
					      fn: "関数"
 | 
				
			||||||
 | 
					    blocks:
 | 
				
			||||||
 | 
					      text: "テキスト"
 | 
				
			||||||
 | 
					      multiLineText: "テキスト(複数行)"
 | 
				
			||||||
 | 
					      textList: "テキストのリスト"
 | 
				
			||||||
 | 
					      add: "+ 足す"
 | 
				
			||||||
 | 
					      _add:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      subtract: "- 引く"
 | 
				
			||||||
 | 
					      _subtract:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      multiply: "× 掛ける"
 | 
				
			||||||
 | 
					      _multiply:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      divide: "÷ 割る"
 | 
				
			||||||
 | 
					      _divide:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      eq: "AとBが同じ"
 | 
				
			||||||
 | 
					      _eq:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      notEq: "AとBが異なる"
 | 
				
			||||||
 | 
					      _notEq:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      and: "AかつB"
 | 
				
			||||||
 | 
					      _and:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      or: "AまたはB"
 | 
				
			||||||
 | 
					      _or:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      lt: "< AがBより小さい"
 | 
				
			||||||
 | 
					      _lt:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      gt: "> AがBより大きい"
 | 
				
			||||||
 | 
					      _gt:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      ltEq: "<= AがBと同じか小さい"
 | 
				
			||||||
 | 
					      _ltEq:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      gtEq: ">= AがBと同じか大きい"
 | 
				
			||||||
 | 
					      _gtEq:
 | 
				
			||||||
 | 
					        arg1: "A"
 | 
				
			||||||
 | 
					        arg2: "B"
 | 
				
			||||||
 | 
					      if: "分岐"
 | 
				
			||||||
 | 
					      _if:
 | 
				
			||||||
 | 
					        arg1: "もし"
 | 
				
			||||||
 | 
					        arg2: "なら"
 | 
				
			||||||
 | 
					        arg3: "そうでなければ"
 | 
				
			||||||
 | 
					      not: "否定"
 | 
				
			||||||
 | 
					      _not:
 | 
				
			||||||
 | 
					        arg1: "否定"
 | 
				
			||||||
 | 
					      random: "ランダム"
 | 
				
			||||||
 | 
					      _random:
 | 
				
			||||||
 | 
					        arg1: "確率"
 | 
				
			||||||
 | 
					      rannum: "乱数"
 | 
				
			||||||
 | 
					      _rannum:
 | 
				
			||||||
 | 
					        arg1: "最小"
 | 
				
			||||||
 | 
					        arg2: "最大"
 | 
				
			||||||
 | 
					      randomPick: "リストからランダムに選択"
 | 
				
			||||||
 | 
					      _randomPick:
 | 
				
			||||||
 | 
					        arg1: "リスト"
 | 
				
			||||||
 | 
					      dailyRandom: "ランダム (ユーザーごとに日替わり)"
 | 
				
			||||||
 | 
					      _dailyRandom:
 | 
				
			||||||
 | 
					        arg1: "確率"
 | 
				
			||||||
 | 
					      dailyRannum: "乱数 (ユーザーごとに日替わり)"
 | 
				
			||||||
 | 
					      _dailyRannum:
 | 
				
			||||||
 | 
					        arg1: "最小"
 | 
				
			||||||
 | 
					        arg2: "最大"
 | 
				
			||||||
 | 
					      dailyRandomPick: "リストからランダムに選択 (ユーザーごとに日替わり)"
 | 
				
			||||||
 | 
					      _dailyRandomPick:
 | 
				
			||||||
 | 
					        arg1: "リスト"
 | 
				
			||||||
 | 
					      number: "数"
 | 
				
			||||||
 | 
					      ref: "変数"
 | 
				
			||||||
 | 
					      in: "入力"
 | 
				
			||||||
 | 
					      _in:
 | 
				
			||||||
 | 
					        arg1: "スロット番号"
 | 
				
			||||||
 | 
					      fn: "関数"
 | 
				
			||||||
 | 
					      _fn:
 | 
				
			||||||
 | 
					        arg1: "出力"
 | 
				
			||||||
 | 
					    typeError: "スロット{slot}は\"{expect}\"を受け付けますが、\"{actual}\"が入れられています!"
 | 
				
			||||||
 | 
					    thereIsEmptySlot: "スロット{slot}が空です!"
 | 
				
			||||||
 | 
					    types:
 | 
				
			||||||
 | 
					      string: "テキスト"
 | 
				
			||||||
 | 
					      number: "数値"
 | 
				
			||||||
 | 
					      boolean: "フラグ"
 | 
				
			||||||
 | 
					      array: "リスト"
 | 
				
			||||||
 | 
					      stringArray: "テキストのリスト"
 | 
				
			||||||
 | 
					    emptySlot: "空のスロット"
 | 
				
			||||||
 | 
					    enviromentVariables: "環境変数"
 | 
				
			||||||
 | 
					    pageVariables: "ページ要素"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										31
									
								
								migration/1556348509290-Pages.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								migration/1556348509290-Pages.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					import {MigrationInterface, QueryRunner} from "typeorm";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Pages1556348509290 implements MigrationInterface {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async up(queryRunner: QueryRunner): Promise<any> {
 | 
				
			||||||
 | 
					        await queryRunner.query(`CREATE TYPE "page_visibility_enum" AS ENUM('public', 'followers', 'specified')`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`CREATE TABLE "page" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "name" character varying(256) NOT NULL, "summary" character varying(256), "alignCenter" boolean NOT NULL, "font" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "eyeCatchingImageId" character varying(32), "content" jsonb NOT NULL DEFAULT '[]', "variables" jsonb NOT NULL DEFAULT '[]', "visibility" "page_visibility_enum" NOT NULL, "visibleUserIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_742f4117e065c5b6ad21b37ba1f" PRIMARY KEY ("id"))`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`CREATE INDEX "IDX_fbb4297c927a9b85e9cefa2eb1" ON "page" ("createdAt") `);
 | 
				
			||||||
 | 
					        await queryRunner.query(`CREATE INDEX "IDX_af639b066dfbca78b01a920f8a" ON "page" ("updatedAt") `);
 | 
				
			||||||
 | 
					        await queryRunner.query(`CREATE INDEX "IDX_b82c19c08afb292de4600d99e4" ON "page" ("name") `);
 | 
				
			||||||
 | 
					        await queryRunner.query(`CREATE INDEX "IDX_ae1d917992dd0c9d9bbdad06c4" ON "page" ("userId") `);
 | 
				
			||||||
 | 
					        await queryRunner.query(`CREATE INDEX "IDX_90148bbc2bf0854428786bfc15" ON "page" ("visibleUserIds") `);
 | 
				
			||||||
 | 
					        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2133ef8317e4bdb839c0dcbf13" ON "page" ("userId", "name") `);
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async down(queryRunner: QueryRunner): Promise<any> {
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`DROP INDEX "IDX_2133ef8317e4bdb839c0dcbf13"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`DROP INDEX "IDX_90148bbc2bf0854428786bfc15"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`DROP INDEX "IDX_ae1d917992dd0c9d9bbdad06c4"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`DROP INDEX "IDX_b82c19c08afb292de4600d99e4"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`DROP INDEX "IDX_af639b066dfbca78b01a920f8a"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`DROP INDEX "IDX_fbb4297c927a9b85e9cefa2eb1"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`DROP TABLE "page"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`DROP TYPE "page_visibility_enum"`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	"name": "misskey",
 | 
						"name": "misskey",
 | 
				
			||||||
	"author": "syuilo <i@syuilo.com>",
 | 
						"author": "syuilo <i@syuilo.com>",
 | 
				
			||||||
	"version": "11.4.0",
 | 
						"version": "11.5.0",
 | 
				
			||||||
	"codename": "daybreak",
 | 
						"codename": "daybreak",
 | 
				
			||||||
	"repository": {
 | 
						"repository": {
 | 
				
			||||||
		"type": "git",
 | 
							"type": "git",
 | 
				
			||||||
| 
						 | 
					@ -199,6 +199,7 @@
 | 
				
			||||||
		"rimraf": "2.6.3",
 | 
							"rimraf": "2.6.3",
 | 
				
			||||||
		"rndstr": "1.0.0",
 | 
							"rndstr": "1.0.0",
 | 
				
			||||||
		"s-age": "1.1.2",
 | 
							"s-age": "1.1.2",
 | 
				
			||||||
 | 
							"seedrandom": "3.0.1",
 | 
				
			||||||
		"sharp": "0.22.0",
 | 
							"sharp": "0.22.0",
 | 
				
			||||||
		"showdown": "1.9.0",
 | 
							"showdown": "1.9.0",
 | 
				
			||||||
		"showdown-highlightjs-extension": "0.1.2",
 | 
							"showdown-highlightjs-extension": "0.1.2",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										470
									
								
								src/client/app/common/scripts/aiscript.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										470
									
								
								src/client/app/common/scripts/aiscript.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,470 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * AiScript
 | 
				
			||||||
 | 
					 * evaluator & type checker
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import autobind from 'autobind-decorator';
 | 
				
			||||||
 | 
					import * as seedrandom from 'seedrandom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						faSuperscript,
 | 
				
			||||||
 | 
						faAlignLeft,
 | 
				
			||||||
 | 
						faShareAlt,
 | 
				
			||||||
 | 
						faSquareRootAlt,
 | 
				
			||||||
 | 
						faPlus,
 | 
				
			||||||
 | 
						faMinus,
 | 
				
			||||||
 | 
						faTimes,
 | 
				
			||||||
 | 
						faDivide,
 | 
				
			||||||
 | 
						faList,
 | 
				
			||||||
 | 
						faQuoteRight,
 | 
				
			||||||
 | 
						faEquals,
 | 
				
			||||||
 | 
						faGreaterThan,
 | 
				
			||||||
 | 
						faLessThan,
 | 
				
			||||||
 | 
						faGreaterThanEqual,
 | 
				
			||||||
 | 
						faLessThanEqual,
 | 
				
			||||||
 | 
						faExclamation,
 | 
				
			||||||
 | 
						faNotEqual,
 | 
				
			||||||
 | 
						faDice,
 | 
				
			||||||
 | 
						faSortNumericUp,
 | 
				
			||||||
 | 
					} from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import { faFlag } from '@fortawesome/free-regular-svg-icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { version } from '../../config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Block = {
 | 
				
			||||||
 | 
						id: string;
 | 
				
			||||||
 | 
						type: string;
 | 
				
			||||||
 | 
						args: Block[];
 | 
				
			||||||
 | 
						value: any;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Variable = Block & {
 | 
				
			||||||
 | 
						name: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Type = 'string' | 'number' | 'boolean' | 'stringArray';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TypeError = {
 | 
				
			||||||
 | 
						arg: number;
 | 
				
			||||||
 | 
						expect: Type;
 | 
				
			||||||
 | 
						actual: Type;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const funcDefs = {
 | 
				
			||||||
 | 
						if:              { in: ['boolean', 0, 0],      out: 0,         category: 'flow',       icon: faShareAlt, },
 | 
				
			||||||
 | 
						not:             { in: ['boolean'],            out: 'boolean', category: 'logical',    icon: faFlag, },
 | 
				
			||||||
 | 
						or:              { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical',    icon: faFlag, },
 | 
				
			||||||
 | 
						and:             { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical',    icon: faFlag, },
 | 
				
			||||||
 | 
						add:             { in: ['number', 'number'],   out: 'number',  category: 'operation',  icon: faPlus, },
 | 
				
			||||||
 | 
						subtract:        { in: ['number', 'number'],   out: 'number',  category: 'operation',  icon: faMinus, },
 | 
				
			||||||
 | 
						multiply:        { in: ['number', 'number'],   out: 'number',  category: 'operation',  icon: faTimes, },
 | 
				
			||||||
 | 
						divide:          { in: ['number', 'number'],   out: 'number',  category: 'operation',  icon: faDivide, },
 | 
				
			||||||
 | 
						eq:              { in: [0, 0],                 out: 'boolean', category: 'comparison', icon: faEquals, },
 | 
				
			||||||
 | 
						notEq:           { in: [0, 0],                 out: 'boolean', category: 'comparison', icon: faNotEqual, },
 | 
				
			||||||
 | 
						gt:              { in: ['number', 'number'],   out: 'boolean', category: 'comparison', icon: faGreaterThan, },
 | 
				
			||||||
 | 
						lt:              { in: ['number', 'number'],   out: 'boolean', category: 'comparison', icon: faLessThan, },
 | 
				
			||||||
 | 
						gtEq:            { in: ['number', 'number'],   out: 'boolean', category: 'comparison', icon: faGreaterThanEqual, },
 | 
				
			||||||
 | 
						ltEq:            { in: ['number', 'number'],   out: 'boolean', category: 'comparison', icon: faLessThanEqual, },
 | 
				
			||||||
 | 
						rannum:          { in: ['number', 'number'],   out: 'number',  category: 'random',     icon: faDice, },
 | 
				
			||||||
 | 
						random:          { in: ['number'],             out: 'boolean', category: 'random',     icon: faDice, },
 | 
				
			||||||
 | 
						randomPick:      { in: [0],                    out: 0,         category: 'random',     icon: faDice, },
 | 
				
			||||||
 | 
						dailyRannum:     { in: ['number', 'number'],   out: 'number',  category: 'random',     icon: faDice, },
 | 
				
			||||||
 | 
						dailyRandom:     { in: ['number'],             out: 'boolean', category: 'random',     icon: faDice, },
 | 
				
			||||||
 | 
						dailyRandomPick: { in: [0],                    out: 0,         category: 'random',     icon: faDice, },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const blockDefs = [
 | 
				
			||||||
 | 
						{ type: 'text',          out: 'string',      category: 'value', icon: faQuoteRight, },
 | 
				
			||||||
 | 
						{ type: 'multiLineText', out: 'string',      category: 'value', icon: faAlignLeft, },
 | 
				
			||||||
 | 
						{ type: 'textList',      out: 'stringArray', category: 'value', icon: faList, },
 | 
				
			||||||
 | 
						{ type: 'number',        out: 'number',      category: 'value', icon: faSortNumericUp, },
 | 
				
			||||||
 | 
						{ type: 'ref',           out: null,          category: 'value', icon: faSuperscript, },
 | 
				
			||||||
 | 
						{ type: 'in',            out: null,          category: 'value', icon: faSuperscript, },
 | 
				
			||||||
 | 
						{ type: 'fn',            out: 'function',    category: 'value', icon: faSuperscript, },
 | 
				
			||||||
 | 
						...Object.entries(funcDefs).map(([k, v]) => ({
 | 
				
			||||||
 | 
							type: k, out: v.out || null, category: v.category, icon: v.icon
 | 
				
			||||||
 | 
						}))
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PageVar = { name: string; value: any; type: Type; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const envVarsDef = {
 | 
				
			||||||
 | 
						AI: 'string',
 | 
				
			||||||
 | 
						VERSION: 'string',
 | 
				
			||||||
 | 
						LOGIN: 'boolean',
 | 
				
			||||||
 | 
						NAME: 'string',
 | 
				
			||||||
 | 
						USERNAME: 'string',
 | 
				
			||||||
 | 
						USERID: 'string',
 | 
				
			||||||
 | 
						NOTES_COUNT: 'number',
 | 
				
			||||||
 | 
						FOLLOWERS_COUNT: 'number',
 | 
				
			||||||
 | 
						FOLLOWING_COUNT: 'number',
 | 
				
			||||||
 | 
						IS_CAT: 'boolean',
 | 
				
			||||||
 | 
						MY_NOTES_COUNT: 'number',
 | 
				
			||||||
 | 
						MY_FOLLOWERS_COUNT: 'number',
 | 
				
			||||||
 | 
						MY_FOLLOWING_COUNT: 'number',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AiScript {
 | 
				
			||||||
 | 
						private variables: Variable[];
 | 
				
			||||||
 | 
						private pageVars: PageVar[];
 | 
				
			||||||
 | 
						private envVars: Record<keyof typeof envVarsDef, any>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static envVarsDef = envVarsDef;
 | 
				
			||||||
 | 
						public static blockDefs = blockDefs;
 | 
				
			||||||
 | 
						public static funcDefs = funcDefs;
 | 
				
			||||||
 | 
						private opts: {
 | 
				
			||||||
 | 
							randomSeed?: string; user?: any; visitor?: any;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(variables: Variable[] = [], pageVars: PageVar[] = [], opts: AiScript['opts'] = {}) {
 | 
				
			||||||
 | 
							this.variables = variables;
 | 
				
			||||||
 | 
							this.pageVars = pageVars;
 | 
				
			||||||
 | 
							this.opts = opts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.envVars = {
 | 
				
			||||||
 | 
								AI: 'kawaii',
 | 
				
			||||||
 | 
								VERSION: version,
 | 
				
			||||||
 | 
								LOGIN: opts.visitor != null,
 | 
				
			||||||
 | 
								NAME: opts.visitor ? opts.visitor.name : '',
 | 
				
			||||||
 | 
								USERNAME: opts.visitor ? opts.visitor.username : '',
 | 
				
			||||||
 | 
								USERID: opts.visitor ? opts.visitor.id : '',
 | 
				
			||||||
 | 
								NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0,
 | 
				
			||||||
 | 
								FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0,
 | 
				
			||||||
 | 
								FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0,
 | 
				
			||||||
 | 
								IS_CAT: opts.visitor ? opts.visitor.isCat : false,
 | 
				
			||||||
 | 
								MY_NOTES_COUNT: opts.user ? opts.user.notesCount : 0,
 | 
				
			||||||
 | 
								MY_FOLLOWERS_COUNT: opts.user ? opts.user.followersCount : 0,
 | 
				
			||||||
 | 
								MY_FOLLOWING_COUNT: opts.user ? opts.user.followingCount : 0,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public injectVars(vars: Variable[]) {
 | 
				
			||||||
 | 
							this.variables = vars;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public injectPageVars(pageVars: PageVar[]) {
 | 
				
			||||||
 | 
							this.pageVars = pageVars;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public updatePageVar(name: string, value: any) {
 | 
				
			||||||
 | 
							this.pageVars.find(v => v.name === name).value = value;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public updateRandomSeed(seed: string) {
 | 
				
			||||||
 | 
							this.opts.randomSeed = seed;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public static isLiteralBlock(v: Block) {
 | 
				
			||||||
 | 
							if (v.type === null) return true;
 | 
				
			||||||
 | 
							if (v.type === 'text') return true;
 | 
				
			||||||
 | 
							if (v.type === 'multiLineText') return true;
 | 
				
			||||||
 | 
							if (v.type === 'textList') return true;
 | 
				
			||||||
 | 
							if (v.type === 'number') return true;
 | 
				
			||||||
 | 
							if (v.type === 'ref') return true;
 | 
				
			||||||
 | 
							if (v.type === 'fn') return true;
 | 
				
			||||||
 | 
							if (v.type === 'in') return true;
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public typeCheck(v: Block): TypeError | null {
 | 
				
			||||||
 | 
							if (AiScript.isLiteralBlock(v)) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const def = AiScript.funcDefs[v.type];
 | 
				
			||||||
 | 
							if (def == null) {
 | 
				
			||||||
 | 
								throw new Error('Unknown type: ' + v.type);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const generic: Type[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (let i = 0; i < def.in.length; i++) {
 | 
				
			||||||
 | 
								const arg = def.in[i];
 | 
				
			||||||
 | 
								const type = this.typeInference(v.args[i]);
 | 
				
			||||||
 | 
								if (type === null) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (typeof arg === 'number') {
 | 
				
			||||||
 | 
									if (generic[arg] === undefined) {
 | 
				
			||||||
 | 
										generic[arg] = type;
 | 
				
			||||||
 | 
									} else if (type !== generic[arg]) {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											arg: i,
 | 
				
			||||||
 | 
											expect: generic[arg],
 | 
				
			||||||
 | 
											actual: type
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else if (type !== arg) {
 | 
				
			||||||
 | 
									return {
 | 
				
			||||||
 | 
										arg: i,
 | 
				
			||||||
 | 
										expect: arg,
 | 
				
			||||||
 | 
										actual: type
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public getExpectedType(v: Block, slot: number): Type | null {
 | 
				
			||||||
 | 
							const def = AiScript.funcDefs[v.type];
 | 
				
			||||||
 | 
							if (def == null) {
 | 
				
			||||||
 | 
								throw new Error('Unknown type: ' + v.type);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const generic: Type[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (let i = 0; i < def.in.length; i++) {
 | 
				
			||||||
 | 
								const arg = def.in[i];
 | 
				
			||||||
 | 
								const type = this.typeInference(v.args[i]);
 | 
				
			||||||
 | 
								if (type === null) continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (typeof arg === 'number') {
 | 
				
			||||||
 | 
									if (generic[arg] === undefined) {
 | 
				
			||||||
 | 
										generic[arg] = type;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (typeof def.in[slot] === 'number') {
 | 
				
			||||||
 | 
								return generic[def.in[slot]] || null;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return def.in[slot];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public typeInference(v: Block): Type | null {
 | 
				
			||||||
 | 
							if (v.type === null) return null;
 | 
				
			||||||
 | 
							if (v.type === 'text') return 'string';
 | 
				
			||||||
 | 
							if (v.type === 'multiLineText') return 'string';
 | 
				
			||||||
 | 
							if (v.type === 'textList') return 'stringArray';
 | 
				
			||||||
 | 
							if (v.type === 'number') return 'number';
 | 
				
			||||||
 | 
							if (v.type === 'ref') {
 | 
				
			||||||
 | 
								const variable = this.variables.find(va => va.name === v.value);
 | 
				
			||||||
 | 
								if (variable) {
 | 
				
			||||||
 | 
									return this.typeInference(variable);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const pageVar = this.pageVars.find(va => va.name === v.value);
 | 
				
			||||||
 | 
								if (pageVar) {
 | 
				
			||||||
 | 
									return pageVar.type;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const envVar = AiScript.envVarsDef[v.value];
 | 
				
			||||||
 | 
								if (envVar) {
 | 
				
			||||||
 | 
									return envVar;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (v.type === 'fn') return null; // todo
 | 
				
			||||||
 | 
							if (v.type === 'in') return null; // todo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const generic: Type[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const def = AiScript.funcDefs[v.type];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (let i = 0; i < def.in.length; i++) {
 | 
				
			||||||
 | 
								const arg = def.in[i];
 | 
				
			||||||
 | 
								if (typeof arg === 'number') {
 | 
				
			||||||
 | 
									const type = this.typeInference(v.args[i]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if (generic[arg] === undefined) {
 | 
				
			||||||
 | 
										generic[arg] = type;
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										if (type !== generic[arg]) {
 | 
				
			||||||
 | 
											generic[arg] = null;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (typeof def.out === 'number') {
 | 
				
			||||||
 | 
								return generic[def.out];
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return def.out;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public getVarsByType(type: Type | null): Variable[] {
 | 
				
			||||||
 | 
							if (type == null) return this.variables;
 | 
				
			||||||
 | 
							return this.variables.filter(x => (this.typeInference(x) === null) || (this.typeInference(x) === type));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public getVarByName(name: string): Variable {
 | 
				
			||||||
 | 
							return this.variables.find(x => x.name === name);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public getEnvVarsByType(type: Type | null): string[] {
 | 
				
			||||||
 | 
							if (type == null) return Object.keys(AiScript.envVarsDef);
 | 
				
			||||||
 | 
							return Object.entries(AiScript.envVarsDef).filter(([k, v]) => type === v).map(([k, v]) => k);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public getPageVarsByType(type: Type | null): string[] {
 | 
				
			||||||
 | 
							if (type == null) return this.pageVars.map(v => v.name);
 | 
				
			||||||
 | 
							return this.pageVars.filter(v => type === v.type).map(v => v.name);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						private interpolate(str: string, values: { name: string, value: any }[]) {
 | 
				
			||||||
 | 
							return str.replace(/\{(.+?)\}/g, match =>
 | 
				
			||||||
 | 
								(this.getVariableValue(match.slice(1, -1).trim(), values) || '').toString());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public evaluateVars() {
 | 
				
			||||||
 | 
							const values: { name: string, value: any }[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (const v of this.variables) {
 | 
				
			||||||
 | 
								values.push({
 | 
				
			||||||
 | 
									name: v.name,
 | 
				
			||||||
 | 
									value: this.evaluate(v, values)
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (const v of this.pageVars) {
 | 
				
			||||||
 | 
								values.push({
 | 
				
			||||||
 | 
									name: v.name,
 | 
				
			||||||
 | 
									value: v.value
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (const [k, v] of Object.entries(this.envVars)) {
 | 
				
			||||||
 | 
								values.push({
 | 
				
			||||||
 | 
									name: k,
 | 
				
			||||||
 | 
									value: v
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return values;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						private evaluate(block: Block, values: { name: string, value: any }[], slotArg: Record<string, any> = {}): any {
 | 
				
			||||||
 | 
							if (block.type === null) {
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (block.type === 'number') {
 | 
				
			||||||
 | 
								return parseInt(block.value, 10);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (block.type === 'text' || block.type === 'multiLineText') {
 | 
				
			||||||
 | 
								return this.interpolate(block.value, values);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (block.type === 'textList') {
 | 
				
			||||||
 | 
								return block.value.trim().split('\n');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (block.type === 'ref') {
 | 
				
			||||||
 | 
								return this.getVariableValue(block.value, values);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (block.type === 'in') {
 | 
				
			||||||
 | 
								return slotArg[block.value];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (block.type === 'fn') { // ユーザー関数定義
 | 
				
			||||||
 | 
								return {
 | 
				
			||||||
 | 
									slots: block.value.slots,
 | 
				
			||||||
 | 
									exec: slotArg => this.evaluate(block.value.expression, values, slotArg)
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (block.type.startsWith('fn:')) { // ユーザー関数呼び出し
 | 
				
			||||||
 | 
								const fnName = block.type.split(':')[1];
 | 
				
			||||||
 | 
								const fn = this.getVariableValue(fnName, values);
 | 
				
			||||||
 | 
								for (let i = 0; i < fn.slots.length; i++) {
 | 
				
			||||||
 | 
									const name = fn.slots[i];
 | 
				
			||||||
 | 
									slotArg[name] = this.evaluate(block.args[i], values);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return fn.exec(slotArg);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (block.args === undefined) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const date = new Date();
 | 
				
			||||||
 | 
							const day = `${this.opts.visitor ? this.opts.visitor.id : ''} ${date.getFullYear()}/${date.getMonth()}/${date.getDate()}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const funcs: { [p in keyof typeof funcDefs]: any } = {
 | 
				
			||||||
 | 
								not: (a) => !a,
 | 
				
			||||||
 | 
								eq: (a, b) => a === b,
 | 
				
			||||||
 | 
								notEq: (a, b) => a !== b,
 | 
				
			||||||
 | 
								gt: (a, b) => a > b,
 | 
				
			||||||
 | 
								lt: (a, b) => a < b,
 | 
				
			||||||
 | 
								gtEq: (a, b) => a >= b,
 | 
				
			||||||
 | 
								ltEq: (a, b) => a <= b,
 | 
				
			||||||
 | 
								or: (a, b) => a || b,
 | 
				
			||||||
 | 
								and: (a, b) => a && b,
 | 
				
			||||||
 | 
								if: (bool, a, b) => bool ? a : b,
 | 
				
			||||||
 | 
								add: (a, b) => a + b,
 | 
				
			||||||
 | 
								subtract: (a, b) => a - b,
 | 
				
			||||||
 | 
								multiply: (a, b) => a * b,
 | 
				
			||||||
 | 
								divide: (a, b) => a / b,
 | 
				
			||||||
 | 
								random: (probability) => Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * 100) < probability,
 | 
				
			||||||
 | 
								rannum: (min, max) => min + Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * (max - min + 1)),
 | 
				
			||||||
 | 
								randomPick: (list) => list[Math.floor(seedrandom(`${this.opts.randomSeed}:${block.id}`)() * list.length)],
 | 
				
			||||||
 | 
								dailyRandom: (probability) => Math.floor(seedrandom(`${day}:${block.id}`)() * 100) < probability,
 | 
				
			||||||
 | 
								dailyRannum: (min, max) => min + Math.floor(seedrandom(`${day}:${block.id}`)() * (max - min + 1)),
 | 
				
			||||||
 | 
								dailyRandomPick: (list) => list[Math.floor(seedrandom(`${day}:${block.id}`)() * list.length)],
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fnName = block.type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const fn = funcs[fnName];
 | 
				
			||||||
 | 
							if (fn == null) {
 | 
				
			||||||
 | 
								console.error('Unknown function: ' + fnName);
 | 
				
			||||||
 | 
								throw new Error('Unknown function: ' + fnName);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const args = block.args.map(x => this.evaluate(x, values, slotArg));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return fn(...args);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						private getVariableValue(name: string, values: { name: string, value: any }[]): any {
 | 
				
			||||||
 | 
							const v = values.find(v => v.name === name);
 | 
				
			||||||
 | 
							if (v) {
 | 
				
			||||||
 | 
								return v.value;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const pageVar = this.pageVars.find(v => v.name === name);
 | 
				
			||||||
 | 
							if (pageVar) {
 | 
				
			||||||
 | 
								return pageVar.value;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (AiScript.envVarsDef[name]) {
 | 
				
			||||||
 | 
								return this.envVars[name].value;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							throw new Error(`Script: No such variable '${name}'`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@autobind
 | 
				
			||||||
 | 
						public isUsedName(name: string) {
 | 
				
			||||||
 | 
							if (this.variables.some(v => v.name === name)) {
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (this.pageVars.some(v => v.name === name)) {
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (AiScript.envVarsDef[name]) {
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								src/client/app/common/scripts/collect-page-vars.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/client/app/common/scripts/collect-page-vars.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					export function collectPageVars(content) {
 | 
				
			||||||
 | 
						const pageVars = [];
 | 
				
			||||||
 | 
						const collect = (xs: any[]) => {
 | 
				
			||||||
 | 
							for (const x of xs) {
 | 
				
			||||||
 | 
								if (x.type === 'input') {
 | 
				
			||||||
 | 
									pageVars.push({
 | 
				
			||||||
 | 
										name: x.name,
 | 
				
			||||||
 | 
										type: x.inputType,
 | 
				
			||||||
 | 
										value: x.default
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else if (x.type === 'switch') {
 | 
				
			||||||
 | 
									pageVars.push({
 | 
				
			||||||
 | 
										name: x.name,
 | 
				
			||||||
 | 
										type: 'boolean',
 | 
				
			||||||
 | 
										value: x.default
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else if (x.children) {
 | 
				
			||||||
 | 
									collect(x.children);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
						collect(content);
 | 
				
			||||||
 | 
						return pageVars;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,14 @@
 | 
				
			||||||
			<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
 | 
								<ui-input v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"></ui-input>
 | 
				
			||||||
			<ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input>
 | 
								<ui-input v-if="user" v-model="userInputValue" autofocus @keydown="onInputKeydown"><template #prefix>@</template></ui-input>
 | 
				
			||||||
			<ui-select v-if="select" v-model="selectedValue" autofocus>
 | 
								<ui-select v-if="select" v-model="selectedValue" autofocus>
 | 
				
			||||||
				<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
 | 
									<template v-if="select.items">
 | 
				
			||||||
 | 
										<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
 | 
				
			||||||
 | 
									</template>
 | 
				
			||||||
 | 
									<template v-else>
 | 
				
			||||||
 | 
										<optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
 | 
				
			||||||
 | 
											<option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
 | 
				
			||||||
 | 
										</optgroup>
 | 
				
			||||||
 | 
									</template>
 | 
				
			||||||
			</ui-select>
 | 
								</ui-select>
 | 
				
			||||||
			<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)">
 | 
								<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)">
 | 
				
			||||||
				<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
 | 
									<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
 | 
				
			||||||
| 
						 | 
					@ -230,7 +237,7 @@ export default Vue.extend({
 | 
				
			||||||
			font-size 32px
 | 
								font-size 32px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			&.success
 | 
								&.success
 | 
				
			||||||
				color #37ec92
 | 
									color #85da5a
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			&.error
 | 
								&.error
 | 
				
			||||||
				color #ec4137
 | 
									color #ec4137
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ export default Vue.extend({
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			hide: true
 | 
								hide: true
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	}
 | 
						},
 | 
				
			||||||
	computed: {
 | 
						computed: {
 | 
				
			||||||
		style(): any {
 | 
							style(): any {
 | 
				
			||||||
			let url = `url(${
 | 
								let url = `url(${
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<component :is="'x-' + value.type" :value="value" @input="v => updateItem(v)" @remove="() => $emit('remove', value)" :key="value.id"/>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import XSection from './page-editor.section.vue';
 | 
				
			||||||
 | 
					import XText from './page-editor.text.vue';
 | 
				
			||||||
 | 
					import XImage from './page-editor.image.vue';
 | 
				
			||||||
 | 
					import XButton from './page-editor.button.vue';
 | 
				
			||||||
 | 
					import XInput from './page-editor.input.vue';
 | 
				
			||||||
 | 
					import XSwitch from './page-editor.switch.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XSection, XText, XImage, XButton, XInput, XSwitch
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,54 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<x-container @remove="() => $emit('remove')">
 | 
				
			||||||
 | 
						<template #header><fa :icon="faBolt"/> {{ $t('blocks.button') }}</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<section class="xfhsjczc">
 | 
				
			||||||
 | 
							<ui-input v-model="value.text"><span>{{ $t('blocks._button.text') }}</span></ui-input>
 | 
				
			||||||
 | 
							<ui-select v-model="value.action">
 | 
				
			||||||
 | 
								<template #label>{{ $t('blocks._button.action') }}</template>
 | 
				
			||||||
 | 
								<option value="dialog">{{ $t('blocks._button._action.dialog') }}</option>
 | 
				
			||||||
 | 
								<option value="resetRandom">{{ $t('blocks._button._action.resetRandom') }}</option>
 | 
				
			||||||
 | 
							</ui-select>
 | 
				
			||||||
 | 
							<ui-input v-if="value.action === 'dialog'" v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					</x-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../../i18n';
 | 
				
			||||||
 | 
					import { faBolt } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import XContainer from './page-editor.container.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n('pages'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XContainer
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								faBolt
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							if (this.value.text == null) Vue.set(this.value, 'text', '');
 | 
				
			||||||
 | 
							if (this.value.action == null) Vue.set(this.value, 'action', 'dialog');
 | 
				
			||||||
 | 
							if (this.value.content == null) Vue.set(this.value, 'content', null);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.xfhsjczc
 | 
				
			||||||
 | 
						padding 0 16px 0 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,135 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div class="cpjygsrt" :class="{ error: error != null, warn: warn != null }">
 | 
				
			||||||
 | 
						<header>
 | 
				
			||||||
 | 
							<div class="title"><slot name="header"></slot></div>
 | 
				
			||||||
 | 
							<div class="buttons">
 | 
				
			||||||
 | 
								<slot name="func"></slot>
 | 
				
			||||||
 | 
								<button v-if="removable" @click="remove()">
 | 
				
			||||||
 | 
									<fa :icon="faTrashAlt"/>
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
								<button @click="toggleContent(!showBody)">
 | 
				
			||||||
 | 
									<template v-if="showBody"><fa icon="angle-up"/></template>
 | 
				
			||||||
 | 
									<template v-else><fa icon="angle-down"/></template>
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</header>
 | 
				
			||||||
 | 
						<p v-show="showBody" class="error" v-if="error != null">{{ $t('script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p>
 | 
				
			||||||
 | 
						<p v-show="showBody" class="warn" v-if="warn != null">{{ $t('script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
 | 
				
			||||||
 | 
						<div v-show="showBody">
 | 
				
			||||||
 | 
							<slot></slot>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
				
			||||||
 | 
					import i18n from '../../../../i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n('pages'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							expanded: {
 | 
				
			||||||
 | 
								type: Boolean,
 | 
				
			||||||
 | 
								default: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							removable: {
 | 
				
			||||||
 | 
								type: Boolean,
 | 
				
			||||||
 | 
								default: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							error: {
 | 
				
			||||||
 | 
								required: false,
 | 
				
			||||||
 | 
								default: null
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							warn: {
 | 
				
			||||||
 | 
								required: false,
 | 
				
			||||||
 | 
								default: null
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								showBody: this.expanded,
 | 
				
			||||||
 | 
								faTrashAlt
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							toggleContent(show: boolean) {
 | 
				
			||||||
 | 
								this.showBody = show;
 | 
				
			||||||
 | 
								this.$emit('toggle', show);
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							remove() {
 | 
				
			||||||
 | 
								this.$emit('remove');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.cpjygsrt
 | 
				
			||||||
 | 
						overflow hidden
 | 
				
			||||||
 | 
						background var(--face)
 | 
				
			||||||
 | 
						border solid 2px var(--pageBlockBorder)
 | 
				
			||||||
 | 
						border-radius 6px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&:hover
 | 
				
			||||||
 | 
							border solid 2px var(--pageBlockBorderHover)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.warn
 | 
				
			||||||
 | 
							border solid 2px #dec44c
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.error
 | 
				
			||||||
 | 
							border solid 2px #f00
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						& + .cpjygsrt
 | 
				
			||||||
 | 
							margin-top 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> header
 | 
				
			||||||
 | 
							> .title
 | 
				
			||||||
 | 
								z-index 1
 | 
				
			||||||
 | 
								margin 0
 | 
				
			||||||
 | 
								padding 0 16px
 | 
				
			||||||
 | 
								line-height 42px
 | 
				
			||||||
 | 
								font-size 0.9em
 | 
				
			||||||
 | 
								font-weight bold
 | 
				
			||||||
 | 
								color var(--faceHeaderText)
 | 
				
			||||||
 | 
								box-shadow 0 1px rgba(#000, 0.07)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> [data-icon]
 | 
				
			||||||
 | 
									margin-right 6px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								&:empty
 | 
				
			||||||
 | 
									display none
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> .buttons
 | 
				
			||||||
 | 
								position absolute
 | 
				
			||||||
 | 
								z-index 2
 | 
				
			||||||
 | 
								top 0
 | 
				
			||||||
 | 
								right 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> button
 | 
				
			||||||
 | 
									padding 0
 | 
				
			||||||
 | 
									width 42px
 | 
				
			||||||
 | 
									font-size 0.9em
 | 
				
			||||||
 | 
									line-height 42px
 | 
				
			||||||
 | 
									color var(--faceTextButton)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&:hover
 | 
				
			||||||
 | 
										color var(--faceTextButtonHover)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&:active
 | 
				
			||||||
 | 
										color var(--faceTextButtonActive)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> .warn
 | 
				
			||||||
 | 
							color #b19e49
 | 
				
			||||||
 | 
							margin 0
 | 
				
			||||||
 | 
							padding 16px 16px 0 16px
 | 
				
			||||||
 | 
							font-size 14px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> .error
 | 
				
			||||||
 | 
							color #f00
 | 
				
			||||||
 | 
							margin 0
 | 
				
			||||||
 | 
							padding 16px 16px 0 16px
 | 
				
			||||||
 | 
							font-size 14px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,78 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<x-container @remove="() => $emit('remove')">
 | 
				
			||||||
 | 
						<template #header><fa :icon="faImage"/> {{ $t('blocks.image') }}</template>
 | 
				
			||||||
 | 
						<template #func>
 | 
				
			||||||
 | 
							<button @click="choose()">
 | 
				
			||||||
 | 
								<fa :icon="faFolderOpen"/>
 | 
				
			||||||
 | 
							</button>
 | 
				
			||||||
 | 
						</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<section class="oyyftmcf">
 | 
				
			||||||
 | 
							<x-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					</x-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../../i18n';
 | 
				
			||||||
 | 
					import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import { faImage, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
 | 
				
			||||||
 | 
					import XContainer from './page-editor.container.vue';
 | 
				
			||||||
 | 
					import XFileThumbnail from '../drive-file-thumbnail.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n('pages'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XContainer, XFileThumbnail
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								file: null,
 | 
				
			||||||
 | 
								faPencilAlt, faImage, faFolderOpen
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							if (this.value.fileId === undefined) Vue.set(this.value, 'fileId', null);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mounted() {
 | 
				
			||||||
 | 
							if (this.value.fileId == null) {
 | 
				
			||||||
 | 
								this.choose();
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								this.$root.api('drive/files/show', {
 | 
				
			||||||
 | 
									fileId: this.value.fileId
 | 
				
			||||||
 | 
								}).then(file => {
 | 
				
			||||||
 | 
									this.file = file;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							async choose() {
 | 
				
			||||||
 | 
								this.$chooseDriveFile({
 | 
				
			||||||
 | 
									multiple: false
 | 
				
			||||||
 | 
								}).then(file => {
 | 
				
			||||||
 | 
									this.file = file;
 | 
				
			||||||
 | 
									this.value.fileId = file.id;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.oyyftmcf
 | 
				
			||||||
 | 
						> .preview
 | 
				
			||||||
 | 
							height 150px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,54 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<x-container @remove="() => $emit('remove')">
 | 
				
			||||||
 | 
						<template #header><fa :icon="faBolt"/> {{ $t('blocks.input') }}</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<section class="dnvasjon">
 | 
				
			||||||
 | 
							<ui-input v-model="value.name"><template #prefix><fa :icon="faSquareRootAlt"/></template><span>{{ $t('blocks._input.name') }}</span></ui-input>
 | 
				
			||||||
 | 
							<ui-input v-model="value.text"><span>{{ $t('blocks._input.text') }}</span></ui-input>
 | 
				
			||||||
 | 
							<ui-select v-model="value.inputType">
 | 
				
			||||||
 | 
								<template #label>{{ $t('blocks._input.inputType') }}</template>
 | 
				
			||||||
 | 
								<option value="string">{{ $t('blocks._input._inputType.string') }}</option>
 | 
				
			||||||
 | 
								<option value="number">{{ $t('blocks._input._inputType.number') }}</option>
 | 
				
			||||||
 | 
							</ui-select>
 | 
				
			||||||
 | 
							<ui-input v-model="value.default" :type="value.inputType"><span>{{ $t('blocks._input.default') }}</span></ui-input>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					</x-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../../i18n';
 | 
				
			||||||
 | 
					import { faBolt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import XContainer from './page-editor.container.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n('pages'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XContainer
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								faBolt, faSquareRootAlt
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							if (this.value.name == null) Vue.set(this.value, 'name', '');
 | 
				
			||||||
 | 
							if (this.value.inputType == null) Vue.set(this.value, 'inputType', 'string');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.dnvasjon
 | 
				
			||||||
 | 
						padding 0 16px 0 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,263 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<x-container :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn">
 | 
				
			||||||
 | 
						<template #header><fa v-if="icon" :icon="icon"/> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template>
 | 
				
			||||||
 | 
						<template #func>
 | 
				
			||||||
 | 
							<button @click="changeType()">
 | 
				
			||||||
 | 
								<fa :icon="faPencilAlt"/>
 | 
				
			||||||
 | 
							</button>
 | 
				
			||||||
 | 
						</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<section v-if="value.type === null" class="pbglfege" @click="changeType()">
 | 
				
			||||||
 | 
							{{ $t('script.emptySlot') }}
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
						<section v-else-if="value.type === 'text'" class="tbwccoaw">
 | 
				
			||||||
 | 
							<input v-model="value.value"/>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
						<section v-else-if="value.type === 'multiLineText'" class="tbwccoaw">
 | 
				
			||||||
 | 
							<textarea v-model="value.value"></textarea>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
						<section v-else-if="value.type === 'textList'" class="frvuzvoi">
 | 
				
			||||||
 | 
							<ui-textarea v-model="value.value"></ui-textarea>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
						<section v-else-if="value.type === 'number'" class="tbwccoaw">
 | 
				
			||||||
 | 
							<input v-model="value.value" type="number"/>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
						<section v-else-if="value.type === 'ref'" class="hpdwcrvs">
 | 
				
			||||||
 | 
							<select v-model="value.value">
 | 
				
			||||||
 | 
								<option v-for="v in aiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option>
 | 
				
			||||||
 | 
								<optgroup :label="$t('script.pageVariables')">
 | 
				
			||||||
 | 
									<option v-for="v in aiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
 | 
				
			||||||
 | 
								</optgroup>
 | 
				
			||||||
 | 
								<optgroup :label="$t('script.enviromentVariables')">
 | 
				
			||||||
 | 
									<option v-for="v in aiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
 | 
				
			||||||
 | 
								</optgroup>
 | 
				
			||||||
 | 
							</select>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
						<section v-else-if="value.type === 'in'" class="hpdwcrvs">
 | 
				
			||||||
 | 
							<select v-model="value.value">
 | 
				
			||||||
 | 
								<option v-for="v in fnSlots" :value="v">{{ v }}</option>
 | 
				
			||||||
 | 
							</select>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
						<section v-else-if="value.type === 'fn'" class="" style="padding:16px;">
 | 
				
			||||||
 | 
							<ui-textarea v-model="slots"></ui-textarea>
 | 
				
			||||||
 | 
							<x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
						<section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;">
 | 
				
			||||||
 | 
							<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i]" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
						<section v-else class="" style="padding:16px;">
 | 
				
			||||||
 | 
							<x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					</x-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../../i18n';
 | 
				
			||||||
 | 
					import XContainer from './page-editor.container.vue';
 | 
				
			||||||
 | 
					import { faSuperscript, faPencilAlt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import { AiScript } from '../../../scripts/aiscript';
 | 
				
			||||||
 | 
					import * as uuid from 'uuid';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n('pages'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XContainer
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						inject: ['getScriptBlockList'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							getExpectedType: {
 | 
				
			||||||
 | 
								required: false,
 | 
				
			||||||
 | 
								default: null
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							title: {
 | 
				
			||||||
 | 
								required: false
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							removable: {
 | 
				
			||||||
 | 
								required: false,
 | 
				
			||||||
 | 
								default: false
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							aiScript: {
 | 
				
			||||||
 | 
								required: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							name: {
 | 
				
			||||||
 | 
								required: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							fnSlots: {
 | 
				
			||||||
 | 
								required: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								AiScript,
 | 
				
			||||||
 | 
								error: null,
 | 
				
			||||||
 | 
								warn: null,
 | 
				
			||||||
 | 
								slots: '',
 | 
				
			||||||
 | 
								faSuperscript, faPencilAlt, faSquareRootAlt
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						computed: {
 | 
				
			||||||
 | 
							icon(): any {
 | 
				
			||||||
 | 
								if (this.value.type === null) return null;
 | 
				
			||||||
 | 
								if (this.value.type.startsWith('fn:')) return null;
 | 
				
			||||||
 | 
								return AiScript.blockDefs.find(x => x.type === this.value.type).icon;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							typeText(): any {
 | 
				
			||||||
 | 
								if (this.value.type === null) return null;
 | 
				
			||||||
 | 
								return this.$t(`script.blocks.${this.value.type}`);
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						watch: {
 | 
				
			||||||
 | 
							slots() {
 | 
				
			||||||
 | 
								this.value.value.slots = this.slots.split('\n');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						beforeCreate() {
 | 
				
			||||||
 | 
							this.$options.components.XV = require('./page-editor.script-block.vue').default;
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							if (this.value.value == null) Vue.set(this.value, 'value', null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (this.value.value && this.value.value.slots) this.slots = this.value.value.slots.join('\n');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.$watch('value.type', (t) => {
 | 
				
			||||||
 | 
								this.warn = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (this.value.type === 'fn') {
 | 
				
			||||||
 | 
									const id = uuid.v4();
 | 
				
			||||||
 | 
									this.value.value = {};
 | 
				
			||||||
 | 
									Vue.set(this.value.value, 'slots', []);
 | 
				
			||||||
 | 
									Vue.set(this.value.value, 'expression', { id, type: null });
 | 
				
			||||||
 | 
									return;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (this.value.type && this.value.type.startsWith('fn:')) {
 | 
				
			||||||
 | 
									const fnName = this.value.type.split(':')[1];
 | 
				
			||||||
 | 
									const fn = this.aiScript.getVarByName(fnName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									const empties = [];
 | 
				
			||||||
 | 
									for (let i = 0; i < fn.value.slots.length; i++) {
 | 
				
			||||||
 | 
										const id = uuid.v4();
 | 
				
			||||||
 | 
										empties.push({ id, type: null });
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									Vue.set(this.value, 'args', empties);
 | 
				
			||||||
 | 
									return;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (AiScript.isLiteralBlock(this.value)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const empties = [];
 | 
				
			||||||
 | 
								for (let i = 0; i < AiScript.funcDefs[this.value.type].in.length; i++) {
 | 
				
			||||||
 | 
									const id = uuid.v4();
 | 
				
			||||||
 | 
									empties.push({ id, type: null });
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								Vue.set(this.value, 'args', empties);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for (let i = 0; i < AiScript.funcDefs[this.value.type].in.length; i++) {
 | 
				
			||||||
 | 
									const inType = AiScript.funcDefs[this.value.type].in[i];
 | 
				
			||||||
 | 
									if (typeof inType !== 'number') {
 | 
				
			||||||
 | 
										if (inType === 'number') this.value.args[i].type = 'number';
 | 
				
			||||||
 | 
										if (inType === 'string') this.value.args[i].type = 'text';
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.$watch('value.args', (args) => {
 | 
				
			||||||
 | 
								if (args == null) {
 | 
				
			||||||
 | 
									this.warn = null;
 | 
				
			||||||
 | 
									return;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								const emptySlotIndex = args.findIndex(x => x.type === null);
 | 
				
			||||||
 | 
								if (emptySlotIndex !== -1 && emptySlotIndex < args.length) {
 | 
				
			||||||
 | 
									this.warn = {
 | 
				
			||||||
 | 
										slot: emptySlotIndex
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									this.warn = null;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								deep: true
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.$watch('aiScript.variables', () => {
 | 
				
			||||||
 | 
								if (this.type != null && this.value) {
 | 
				
			||||||
 | 
									this.error = this.aiScript.typeCheck(this.value);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								deep: true
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							async changeType() {
 | 
				
			||||||
 | 
								const { canceled, result: type } = await this.$root.dialog({
 | 
				
			||||||
 | 
									type: null,
 | 
				
			||||||
 | 
									title: this.$t('select-type'),
 | 
				
			||||||
 | 
									select: {
 | 
				
			||||||
 | 
										groupedItems: this.getScriptBlockList(this.getExpectedType ? this.getExpectedType() : null)
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									showCancelButton: true
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								if (canceled) return;
 | 
				
			||||||
 | 
								this.value.type = type;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_getExpectedType(slot: number) {
 | 
				
			||||||
 | 
								return this.aiScript.getExpectedType(this.value, slot);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.turmquns
 | 
				
			||||||
 | 
						opacity 0.7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pbglfege
 | 
				
			||||||
 | 
						opacity 0.5
 | 
				
			||||||
 | 
						padding 16px
 | 
				
			||||||
 | 
						text-align center
 | 
				
			||||||
 | 
						cursor pointer
 | 
				
			||||||
 | 
						color var(--text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tbwccoaw
 | 
				
			||||||
 | 
						> input
 | 
				
			||||||
 | 
						> textarea
 | 
				
			||||||
 | 
							display block
 | 
				
			||||||
 | 
							-webkit-appearance none
 | 
				
			||||||
 | 
							-moz-appearance none
 | 
				
			||||||
 | 
							appearance none
 | 
				
			||||||
 | 
							width 100%
 | 
				
			||||||
 | 
							max-width 100%
 | 
				
			||||||
 | 
							min-width 100%
 | 
				
			||||||
 | 
							border none
 | 
				
			||||||
 | 
							box-shadow none
 | 
				
			||||||
 | 
							padding 16px
 | 
				
			||||||
 | 
							font-size 16px
 | 
				
			||||||
 | 
							background transparent
 | 
				
			||||||
 | 
							color var(--text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> textarea
 | 
				
			||||||
 | 
							min-height 100px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hpdwcrvs
 | 
				
			||||||
 | 
						padding 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> select
 | 
				
			||||||
 | 
							display block
 | 
				
			||||||
 | 
							padding 4px
 | 
				
			||||||
 | 
							font-size 16px
 | 
				
			||||||
 | 
							width 100%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,133 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<x-container @remove="() => $emit('remove')">
 | 
				
			||||||
 | 
						<template #header><fa :icon="faStickyNote"/> {{ value.title }}</template>
 | 
				
			||||||
 | 
						<template #func>
 | 
				
			||||||
 | 
							<button @click="rename()">
 | 
				
			||||||
 | 
								<fa :icon="faPencilAlt"/>
 | 
				
			||||||
 | 
							</button>
 | 
				
			||||||
 | 
							<button @click="add()">
 | 
				
			||||||
 | 
								<fa :icon="faPlus"/>
 | 
				
			||||||
 | 
							</button>
 | 
				
			||||||
 | 
						</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<section class="ilrvjyvi">
 | 
				
			||||||
 | 
							<div class="children">
 | 
				
			||||||
 | 
								<x-block v-for="child in value.children" :value="child" @input="v => updateItem(v)" @remove="() => remove(child)" :key="child.id"/>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					</x-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../../i18n';
 | 
				
			||||||
 | 
					import { faPlus, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
 | 
				
			||||||
 | 
					import XContainer from './page-editor.container.vue';
 | 
				
			||||||
 | 
					import * as uuid from 'uuid';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n('pages'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XContainer
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								faStickyNote, faPlus, faPencilAlt
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						beforeCreate() {
 | 
				
			||||||
 | 
							this.$options.components.XBlock = require('./page-editor.block.vue').default
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							if (this.value.title == null) Vue.set(this.value, 'title', null);
 | 
				
			||||||
 | 
							if (this.value.children == null) Vue.set(this.value, 'children', []);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mounted() {
 | 
				
			||||||
 | 
							if (this.value.title == null) {
 | 
				
			||||||
 | 
								this.rename();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							async rename() {
 | 
				
			||||||
 | 
								const { canceled, result: title } = await this.$root.dialog({
 | 
				
			||||||
 | 
									title: 'Enter title',
 | 
				
			||||||
 | 
									input: {
 | 
				
			||||||
 | 
										type: 'text',
 | 
				
			||||||
 | 
										default: this.value.title
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									showCancelButton: true
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								if (canceled) return;
 | 
				
			||||||
 | 
								this.value.title = title;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							async add() {
 | 
				
			||||||
 | 
								const { canceled, result: type } = await this.$root.dialog({
 | 
				
			||||||
 | 
									type: null,
 | 
				
			||||||
 | 
									title: this.$t('choose-block'),
 | 
				
			||||||
 | 
									select: {
 | 
				
			||||||
 | 
										items: [{
 | 
				
			||||||
 | 
											value: 'section', text: this.$t('blocks.section')
 | 
				
			||||||
 | 
										}, {
 | 
				
			||||||
 | 
											value: 'text', text: this.$t('blocks.text')
 | 
				
			||||||
 | 
										}, {
 | 
				
			||||||
 | 
											value: 'image', text: this.$t('blocks.image')
 | 
				
			||||||
 | 
										}, {
 | 
				
			||||||
 | 
											value: 'button', text: this.$t('blocks.button')
 | 
				
			||||||
 | 
										}, {
 | 
				
			||||||
 | 
											value: 'input', text: this.$t('blocks.input')
 | 
				
			||||||
 | 
										}, {
 | 
				
			||||||
 | 
											value: 'switch', text: this.$t('blocks.switch')
 | 
				
			||||||
 | 
										}]
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									showCancelButton: true
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								if (canceled) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const id = uuid.v4();
 | 
				
			||||||
 | 
								this.value.children.push({ id, type });
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							updateItem(v) {
 | 
				
			||||||
 | 
								const i = this.value.children.findIndex(x => x.id === v.id);
 | 
				
			||||||
 | 
								const newValue = [
 | 
				
			||||||
 | 
									...this.value.children.slice(0, i),
 | 
				
			||||||
 | 
									v,
 | 
				
			||||||
 | 
									...this.value.children.slice(i + 1)
 | 
				
			||||||
 | 
								];
 | 
				
			||||||
 | 
								this.value.children = newValue;
 | 
				
			||||||
 | 
								this.$emit('input', this.value);
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							remove(el) {
 | 
				
			||||||
 | 
								const i = this.value.children.findIndex(x => x.id === el.id);
 | 
				
			||||||
 | 
								const newValue = [
 | 
				
			||||||
 | 
									...this.value.children.slice(0, i),
 | 
				
			||||||
 | 
									...this.value.children.slice(i + 1)
 | 
				
			||||||
 | 
								];
 | 
				
			||||||
 | 
								this.value.children = newValue;
 | 
				
			||||||
 | 
								this.$emit('input', this.value);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.ilrvjyvi
 | 
				
			||||||
 | 
						> .children
 | 
				
			||||||
 | 
							padding 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,48 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<x-container @remove="() => $emit('remove')">
 | 
				
			||||||
 | 
						<template #header><fa :icon="faBolt"/> {{ $t('blocks.switch') }}</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<section class="kjuadyyj">
 | 
				
			||||||
 | 
							<ui-input v-model="value.name"><template #prefix><fa :icon="faSquareRootAlt"/></template><span>{{ $t('blocks._switch.name') }}</span></ui-input>
 | 
				
			||||||
 | 
							<ui-input v-model="value.text"><span>{{ $t('blocks._switch.text') }}</span></ui-input>
 | 
				
			||||||
 | 
							<ui-switch v-model="value.default"><span>{{ $t('blocks._switch.default') }}</span></ui-switch>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					</x-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../../i18n';
 | 
				
			||||||
 | 
					import { faBolt, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import XContainer from './page-editor.container.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n('pages'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XContainer
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								faBolt, faSquareRootAlt
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							if (this.value.name == null) Vue.set(this.value, 'name', '');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.kjuadyyj
 | 
				
			||||||
 | 
						padding 0 16px 16px 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,57 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<x-container @remove="() => $emit('remove')">
 | 
				
			||||||
 | 
						<template #header><fa :icon="faAlignLeft"/> {{ $t('blocks.text') }}</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<section class="ihymsbbe">
 | 
				
			||||||
 | 
							<textarea v-model="value.text"></textarea>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					</x-container>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../../i18n';
 | 
				
			||||||
 | 
					import { faAlignLeft } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import XContainer from './page-editor.container.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n('pages'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XContainer
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								faAlignLeft,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							if (this.value.text == null) Vue.set(this.value, 'text', '');
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.ihymsbbe
 | 
				
			||||||
 | 
						> textarea
 | 
				
			||||||
 | 
							display block
 | 
				
			||||||
 | 
							-webkit-appearance none
 | 
				
			||||||
 | 
							-moz-appearance none
 | 
				
			||||||
 | 
							appearance none
 | 
				
			||||||
 | 
							width 100%
 | 
				
			||||||
 | 
							min-width 100%
 | 
				
			||||||
 | 
							min-height 150px
 | 
				
			||||||
 | 
							border none
 | 
				
			||||||
 | 
							box-shadow none
 | 
				
			||||||
 | 
							padding 16px
 | 
				
			||||||
 | 
							background transparent
 | 
				
			||||||
 | 
							color var(--text)
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,452 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
						<div class="gwbmwxkm" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
 | 
				
			||||||
 | 
							<header>
 | 
				
			||||||
 | 
								<div class="title"><fa :icon="faStickyNote"/> {{ pageId ? $t('edit-page') : $t('new-page') }}</div>
 | 
				
			||||||
 | 
								<div class="buttons">
 | 
				
			||||||
 | 
									<button @click="del()"><fa :icon="faTrashAlt"/></button>
 | 
				
			||||||
 | 
									<button @click="() => showOptions = !showOptions"><fa :icon="faCog"/></button>
 | 
				
			||||||
 | 
									<button @click="save()"><fa :icon="faSave"/></button>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<section>
 | 
				
			||||||
 | 
								<ui-input v-model="title">
 | 
				
			||||||
 | 
									<span>{{ $t('title') }}</span>
 | 
				
			||||||
 | 
								</ui-input>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<template v-if="showOptions">
 | 
				
			||||||
 | 
									<ui-input v-model="summary">
 | 
				
			||||||
 | 
										<span>{{ $t('summary') }}</span>
 | 
				
			||||||
 | 
									</ui-input>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<ui-input v-model="name">
 | 
				
			||||||
 | 
										<template #prefix>{{ url }}/@{{ $store.state.i.username }}/pages/</template>
 | 
				
			||||||
 | 
										<span>{{ $t('url') }}</span>
 | 
				
			||||||
 | 
									</ui-input>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<ui-switch v-model="alignCenter">{{ $t('align-center') }}</ui-switch>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<ui-select v-model="font">
 | 
				
			||||||
 | 
										<template #label>{{ $t('font') }}</template>
 | 
				
			||||||
 | 
										<option value="serif">{{ $t('fontSerif') }}</option>
 | 
				
			||||||
 | 
										<option value="sans-serif">{{ $t('fontSansSerif') }}</option>
 | 
				
			||||||
 | 
									</ui-select>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<div class="eyeCatch">
 | 
				
			||||||
 | 
										<ui-button v-if="eyeCatchingImageId == null" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catchig-image') }}</ui-button>
 | 
				
			||||||
 | 
										<div v-else-if="eyeCatchingImage">
 | 
				
			||||||
 | 
											<img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name"/>
 | 
				
			||||||
 | 
											<ui-button @click="removeEyeCatchingImage()"><fa :icon="faTrashAlt"/> {{ $t('remove-eye-catchig-image') }}</ui-button>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<div class="content" v-for="child in content">
 | 
				
			||||||
 | 
									<x-block :value="child" @input="v => updateItem(v)" @remove="() => remove(child)" :key="child.id"/>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<ui-button @click="add()"><fa :icon="faPlus"/></ui-button>
 | 
				
			||||||
 | 
							</section>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<ui-container :body-togglable="true">
 | 
				
			||||||
 | 
							<template #header><fa :icon="faSquareRootAlt"/> {{ $t('variables') }}</template>
 | 
				
			||||||
 | 
							<div class="qmuvgica">
 | 
				
			||||||
 | 
								<div class="variables" v-show="variables.length > 0">
 | 
				
			||||||
 | 
									<template v-for="variable in variables">
 | 
				
			||||||
 | 
										<x-variable
 | 
				
			||||||
 | 
											:value="variable"
 | 
				
			||||||
 | 
											:removable="true"
 | 
				
			||||||
 | 
											@input="v => updateVariable(v)"
 | 
				
			||||||
 | 
											@remove="() => removeVariable(variable)"
 | 
				
			||||||
 | 
											:key="variable.name"
 | 
				
			||||||
 | 
											:ai-script="aiScript"
 | 
				
			||||||
 | 
											:name="variable.name"
 | 
				
			||||||
 | 
											:title="variable.name"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</template>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<ui-button @click="addVariable()" class="add"><fa :icon="faPlus"/></ui-button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<ui-info><span v-html="$t('variables-info')"></span><a @click="() => moreDetails = true" style="display:block;">{{ $t('more-details') }}</a></ui-info>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<template v-if="moreDetails">
 | 
				
			||||||
 | 
									<ui-info><span v-html="$t('variables-info2')"></span></ui-info>
 | 
				
			||||||
 | 
									<ui-info><span v-html="$t('variables-info3')"></span></ui-info>
 | 
				
			||||||
 | 
								</template>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</ui-container>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../../i18n';
 | 
				
			||||||
 | 
					import { faICursor, faPlus, faSquareRootAlt, faCog } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
 | 
				
			||||||
 | 
					import XVariable from './page-editor.script-block.vue';
 | 
				
			||||||
 | 
					import XBlock from './page-editor.block.vue';
 | 
				
			||||||
 | 
					import * as uuid from 'uuid';
 | 
				
			||||||
 | 
					import { AiScript } from '../../../scripts/aiscript';
 | 
				
			||||||
 | 
					import { url } from '../../../../config';
 | 
				
			||||||
 | 
					import { collectPageVars } from '../../../scripts/collect-page-vars';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n('pages'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XVariable, XBlock
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							page: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								pageId: null,
 | 
				
			||||||
 | 
								title: '',
 | 
				
			||||||
 | 
								summary: null,
 | 
				
			||||||
 | 
								name: Date.now().toString(),
 | 
				
			||||||
 | 
								eyeCatchingImage: null,
 | 
				
			||||||
 | 
								eyeCatchingImageId: null,
 | 
				
			||||||
 | 
								font: 'sans-serif',
 | 
				
			||||||
 | 
								content: [],
 | 
				
			||||||
 | 
								alignCenter: false,
 | 
				
			||||||
 | 
								variables: [],
 | 
				
			||||||
 | 
								aiScript: null,
 | 
				
			||||||
 | 
								showOptions: false,
 | 
				
			||||||
 | 
								moreDetails: false,
 | 
				
			||||||
 | 
								url,
 | 
				
			||||||
 | 
								faPlus, faICursor, faSave, faStickyNote, faSquareRootAlt, faCog, faTrashAlt
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						watch: {
 | 
				
			||||||
 | 
							async eyeCatchingImageId() {
 | 
				
			||||||
 | 
								if (this.eyeCatchingImageId == null) {
 | 
				
			||||||
 | 
									this.eyeCatchingImage = null;
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									this.eyeCatchingImage = await this.$root.api('drive/files/show', {
 | 
				
			||||||
 | 
										fileId: this.eyeCatchingImageId,
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							this.aiScript = new AiScript();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.$watch('variables', () => {
 | 
				
			||||||
 | 
								this.aiScript.injectVars(this.variables);
 | 
				
			||||||
 | 
							}, { deep: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.$watch('content', () => {
 | 
				
			||||||
 | 
								this.aiScript.injectPageVars(collectPageVars(this.content));
 | 
				
			||||||
 | 
							}, { deep: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (this.page) {
 | 
				
			||||||
 | 
								this.$root.api('pages/show', {
 | 
				
			||||||
 | 
									pageId: this.page,
 | 
				
			||||||
 | 
								}).then(page => {
 | 
				
			||||||
 | 
									this.pageId = page.id;
 | 
				
			||||||
 | 
									this.title = page.title;
 | 
				
			||||||
 | 
									this.name = page.name;
 | 
				
			||||||
 | 
									this.summary = page.summary;
 | 
				
			||||||
 | 
									this.font = page.font;
 | 
				
			||||||
 | 
									this.alignCenter = page.alignCenter;
 | 
				
			||||||
 | 
									this.content = page.content;
 | 
				
			||||||
 | 
									this.variables = page.variables;
 | 
				
			||||||
 | 
									this.eyeCatchingImageId = page.eyeCatchingImageId;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								const id = uuid.v4();
 | 
				
			||||||
 | 
								this.content = [{
 | 
				
			||||||
 | 
									id,
 | 
				
			||||||
 | 
									type: 'text',
 | 
				
			||||||
 | 
									text: 'Hello World!'
 | 
				
			||||||
 | 
								}];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						provide() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								getScriptBlockList: this.getScriptBlockList
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							save() {
 | 
				
			||||||
 | 
								if (this.pageId) {
 | 
				
			||||||
 | 
									this.$root.api('pages/update', {
 | 
				
			||||||
 | 
										pageId: this.pageId,
 | 
				
			||||||
 | 
										title: this.title.trim(),
 | 
				
			||||||
 | 
										name: this.name.trim(),
 | 
				
			||||||
 | 
										summary: this.summary,
 | 
				
			||||||
 | 
										font: this.font,
 | 
				
			||||||
 | 
										alignCenter: this.alignCenter,
 | 
				
			||||||
 | 
										content: this.content,
 | 
				
			||||||
 | 
										variables: this.variables,
 | 
				
			||||||
 | 
										eyeCatchingImageId: this.eyeCatchingImageId,
 | 
				
			||||||
 | 
									}).then(page => {
 | 
				
			||||||
 | 
										this.$root.dialog({
 | 
				
			||||||
 | 
											type: 'success',
 | 
				
			||||||
 | 
											text: this.$t('page-updated')
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									this.$root.api('pages/create', {
 | 
				
			||||||
 | 
										title: this.title.trim(),
 | 
				
			||||||
 | 
										name: this.name.trim(),
 | 
				
			||||||
 | 
										summary: this.summary,
 | 
				
			||||||
 | 
										font: this.font,
 | 
				
			||||||
 | 
										alignCenter: this.alignCenter,
 | 
				
			||||||
 | 
										content: this.content,
 | 
				
			||||||
 | 
										variables: this.variables,
 | 
				
			||||||
 | 
										eyeCatchingImageId: this.eyeCatchingImageId,
 | 
				
			||||||
 | 
									}).then(page => {
 | 
				
			||||||
 | 
										this.pageId = page.id;
 | 
				
			||||||
 | 
										this.$root.dialog({
 | 
				
			||||||
 | 
											type: 'success',
 | 
				
			||||||
 | 
											text: this.$t('page-created')
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
										this.$router.push(`/i/pages/edit/${this.pageId}`);
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							del() {
 | 
				
			||||||
 | 
								this.$root.dialog({
 | 
				
			||||||
 | 
									type: 'warning',
 | 
				
			||||||
 | 
									text: this.$t('are-you-sure-delete'),
 | 
				
			||||||
 | 
									showCancelButton: true
 | 
				
			||||||
 | 
								}).then(({ canceled }) => {
 | 
				
			||||||
 | 
									if (canceled) return;
 | 
				
			||||||
 | 
									this.$root.api('pages/delete', {
 | 
				
			||||||
 | 
										pageId: this.pageId,
 | 
				
			||||||
 | 
									}).then(() => {
 | 
				
			||||||
 | 
										this.$root.dialog({
 | 
				
			||||||
 | 
											type: 'success',
 | 
				
			||||||
 | 
											text: this.$t('page-deleted')
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
										this.$router.push(`/i/pages`);
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							async add() {
 | 
				
			||||||
 | 
								const { canceled, result: type } = await this.$root.dialog({
 | 
				
			||||||
 | 
									type: null,
 | 
				
			||||||
 | 
									title: this.$t('choose-block'),
 | 
				
			||||||
 | 
									select: {
 | 
				
			||||||
 | 
										items: [{
 | 
				
			||||||
 | 
											value: 'section', text: this.$t('blocks.section')
 | 
				
			||||||
 | 
										}, {
 | 
				
			||||||
 | 
											value: 'text', text: this.$t('blocks.text')
 | 
				
			||||||
 | 
										}, {
 | 
				
			||||||
 | 
											value: 'image', text: this.$t('blocks.image')
 | 
				
			||||||
 | 
										}, {
 | 
				
			||||||
 | 
											value: 'button', text: this.$t('blocks.button')
 | 
				
			||||||
 | 
										}, {
 | 
				
			||||||
 | 
											value: 'input', text: this.$t('blocks.input')
 | 
				
			||||||
 | 
										}, {
 | 
				
			||||||
 | 
											value: 'switch', text: this.$t('blocks.switch')
 | 
				
			||||||
 | 
										}]
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									showCancelButton: true
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								if (canceled) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const id = uuid.v4();
 | 
				
			||||||
 | 
								this.content.push({ id, type });
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							async addVariable() {
 | 
				
			||||||
 | 
								let { canceled, result: name } = await this.$root.dialog({
 | 
				
			||||||
 | 
									title: this.$t('enter-variable-name'),
 | 
				
			||||||
 | 
									input: {
 | 
				
			||||||
 | 
										type: 'text',
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									showCancelButton: true
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								if (canceled) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								name = name.trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (this.aiScript.isUsedName(name)) {
 | 
				
			||||||
 | 
									this.$root.dialog({
 | 
				
			||||||
 | 
										type: 'error',
 | 
				
			||||||
 | 
										text: this.$t('the-variable-name-is-already-used')
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
									return;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const id = uuid.v4();
 | 
				
			||||||
 | 
								this.variables.push({ id, name, type: null });
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							updateItem(v) {
 | 
				
			||||||
 | 
								const i = this.content.findIndex(x => x.id === v.id);
 | 
				
			||||||
 | 
								const newValue = [
 | 
				
			||||||
 | 
									...this.content.slice(0, i),
 | 
				
			||||||
 | 
									v,
 | 
				
			||||||
 | 
									...this.content.slice(i + 1)
 | 
				
			||||||
 | 
								];
 | 
				
			||||||
 | 
								this.content = newValue;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							remove(el) {
 | 
				
			||||||
 | 
								const i = this.content.findIndex(x => x.id === el.id);
 | 
				
			||||||
 | 
								const newValue = [
 | 
				
			||||||
 | 
									...this.content.slice(0, i),
 | 
				
			||||||
 | 
									...this.content.slice(i + 1)
 | 
				
			||||||
 | 
								];
 | 
				
			||||||
 | 
								this.content = newValue;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							removeVariable(v) {
 | 
				
			||||||
 | 
								const i = this.variables.findIndex(x => x.name === v.name);
 | 
				
			||||||
 | 
								const newValue = [
 | 
				
			||||||
 | 
									...this.variables.slice(0, i),
 | 
				
			||||||
 | 
									...this.variables.slice(i + 1)
 | 
				
			||||||
 | 
								];
 | 
				
			||||||
 | 
								this.variables = newValue;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							getScriptBlockList(type: string = null) {
 | 
				
			||||||
 | 
								const list = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const blocks = AiScript.blockDefs.filter(block => type === null || block.out === null || block.out === type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for (const block of blocks) {
 | 
				
			||||||
 | 
									const category = list.find(x => x.category === block.category);
 | 
				
			||||||
 | 
									if (category) {
 | 
				
			||||||
 | 
										category.items.push({
 | 
				
			||||||
 | 
											value: block.type,
 | 
				
			||||||
 | 
											text: this.$t(`script.blocks.${block.type}`)
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										list.push({
 | 
				
			||||||
 | 
											category: block.category,
 | 
				
			||||||
 | 
											label: this.$t(`script.categories.${block.category}`),
 | 
				
			||||||
 | 
											items: [{
 | 
				
			||||||
 | 
												value: block.type,
 | 
				
			||||||
 | 
												text: this.$t(`script.blocks.${block.type}`)
 | 
				
			||||||
 | 
											}]
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const userFns = this.variables.filter(x => x.type === 'fn');
 | 
				
			||||||
 | 
								if (userFns.length > 0) {
 | 
				
			||||||
 | 
									list.unshift({
 | 
				
			||||||
 | 
										label: this.$t(`script.categories.fn`),
 | 
				
			||||||
 | 
										items: userFns.map(v => ({
 | 
				
			||||||
 | 
											value: 'fn:' + v.name,
 | 
				
			||||||
 | 
											text: v.name
 | 
				
			||||||
 | 
										}))
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return list;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							setEyeCatchingImage() {
 | 
				
			||||||
 | 
								this.$chooseDriveFile({
 | 
				
			||||||
 | 
									multiple: false
 | 
				
			||||||
 | 
								}).then(file => {
 | 
				
			||||||
 | 
									this.eyeCatchingImageId = file.id;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							removeEyeCatchingImage() {
 | 
				
			||||||
 | 
								this.eyeCatchingImageId = null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.gwbmwxkm
 | 
				
			||||||
 | 
						overflow hidden
 | 
				
			||||||
 | 
						background var(--face)
 | 
				
			||||||
 | 
						margin-bottom 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.round
 | 
				
			||||||
 | 
							border-radius 6px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.shadow
 | 
				
			||||||
 | 
							box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> header
 | 
				
			||||||
 | 
							background var(--faceHeader)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> .title
 | 
				
			||||||
 | 
								z-index 1
 | 
				
			||||||
 | 
								margin 0
 | 
				
			||||||
 | 
								padding 0 16px
 | 
				
			||||||
 | 
								line-height 42px
 | 
				
			||||||
 | 
								font-size 0.9em
 | 
				
			||||||
 | 
								font-weight bold
 | 
				
			||||||
 | 
								color var(--faceHeaderText)
 | 
				
			||||||
 | 
								box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> [data-icon]
 | 
				
			||||||
 | 
									margin-right 6px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								&:empty
 | 
				
			||||||
 | 
									display none
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> .buttons
 | 
				
			||||||
 | 
								position absolute
 | 
				
			||||||
 | 
								z-index 2
 | 
				
			||||||
 | 
								top 0
 | 
				
			||||||
 | 
								right 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> button
 | 
				
			||||||
 | 
									padding 0
 | 
				
			||||||
 | 
									width 42px
 | 
				
			||||||
 | 
									font-size 0.9em
 | 
				
			||||||
 | 
									line-height 42px
 | 
				
			||||||
 | 
									color var(--faceTextButton)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&:hover
 | 
				
			||||||
 | 
										color var(--faceTextButtonHover)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&:active
 | 
				
			||||||
 | 
										color var(--faceTextButtonActive)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> section
 | 
				
			||||||
 | 
							padding 0 32px 32px 32px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@media (max-width 500px)
 | 
				
			||||||
 | 
								padding 0 16px 16px 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> .content
 | 
				
			||||||
 | 
								margin-bottom 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> .eyeCatch
 | 
				
			||||||
 | 
								margin-bottom 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> div
 | 
				
			||||||
 | 
									> img
 | 
				
			||||||
 | 
										max-width 100%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.qmuvgica
 | 
				
			||||||
 | 
						padding 32px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@media (max-width 500px)
 | 
				
			||||||
 | 
							padding 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> .variables
 | 
				
			||||||
 | 
							margin-bottom 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> .add
 | 
				
			||||||
 | 
							margin-bottom 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										141
									
								
								src/client/app/common/views/components/page-preview.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/client/app/common/views/components/page-preview.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,141 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<router-link :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
 | 
				
			||||||
 | 
						<div class="thumbnail" v-if="page.eyeCatchingImage" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
 | 
				
			||||||
 | 
						<article>
 | 
				
			||||||
 | 
							<header>
 | 
				
			||||||
 | 
								<h1 :title="page.title">{{ page.title }}</h1>
 | 
				
			||||||
 | 
							</header>
 | 
				
			||||||
 | 
							<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
 | 
				
			||||||
 | 
							<footer>
 | 
				
			||||||
 | 
								<img class="icon" :src="page.user.avatarUrl"/>
 | 
				
			||||||
 | 
								<p>{{ page.user | userName }}</p>
 | 
				
			||||||
 | 
							</footer>
 | 
				
			||||||
 | 
						</article>
 | 
				
			||||||
 | 
					</router-link>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							page: {
 | 
				
			||||||
 | 
								type: Object,
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.vhpxefrj
 | 
				
			||||||
 | 
						display block
 | 
				
			||||||
 | 
						overflow hidden
 | 
				
			||||||
 | 
						width 100%
 | 
				
			||||||
 | 
						background var(--face)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.round
 | 
				
			||||||
 | 
							border-radius 8px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.shadow
 | 
				
			||||||
 | 
							box-shadow 0 4px 16px rgba(#000, 0.1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@media (min-width 500px)
 | 
				
			||||||
 | 
								box-shadow 0 8px 32px rgba(#000, 0.1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> .thumbnail
 | 
				
			||||||
 | 
							position absolute
 | 
				
			||||||
 | 
							width 100px
 | 
				
			||||||
 | 
							height 100%
 | 
				
			||||||
 | 
							background-position center
 | 
				
			||||||
 | 
							background-size cover
 | 
				
			||||||
 | 
							display flex
 | 
				
			||||||
 | 
							justify-content center
 | 
				
			||||||
 | 
							align-items center
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> button
 | 
				
			||||||
 | 
								font-size 3.5em
 | 
				
			||||||
 | 
								opacity: 0.7
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								&:hover
 | 
				
			||||||
 | 
									font-size 4em
 | 
				
			||||||
 | 
									opacity 0.9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							& + article
 | 
				
			||||||
 | 
								left 100px
 | 
				
			||||||
 | 
								width calc(100% - 100px)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> article
 | 
				
			||||||
 | 
							padding 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> header
 | 
				
			||||||
 | 
								margin-bottom 8px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> h1
 | 
				
			||||||
 | 
									margin 0
 | 
				
			||||||
 | 
									font-size 1em
 | 
				
			||||||
 | 
									color var(--urlPreviewTitle)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> p
 | 
				
			||||||
 | 
								margin 0
 | 
				
			||||||
 | 
								color var(--urlPreviewText)
 | 
				
			||||||
 | 
								font-size 0.8em
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> footer
 | 
				
			||||||
 | 
								margin-top 8px
 | 
				
			||||||
 | 
								height 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> img
 | 
				
			||||||
 | 
									display inline-block
 | 
				
			||||||
 | 
									width 16px
 | 
				
			||||||
 | 
									height 16px
 | 
				
			||||||
 | 
									margin-right 4px
 | 
				
			||||||
 | 
									vertical-align top
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> p
 | 
				
			||||||
 | 
									display inline-block
 | 
				
			||||||
 | 
									margin 0
 | 
				
			||||||
 | 
									color var(--urlPreviewInfo)
 | 
				
			||||||
 | 
									font-size 0.8em
 | 
				
			||||||
 | 
									line-height 16px
 | 
				
			||||||
 | 
									vertical-align top
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@media (max-width 700px)
 | 
				
			||||||
 | 
							> .thumbnail
 | 
				
			||||||
 | 
								position relative
 | 
				
			||||||
 | 
								width 100%
 | 
				
			||||||
 | 
								height 100px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								& + article
 | 
				
			||||||
 | 
									left 0
 | 
				
			||||||
 | 
									width 100%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@media (max-width 550px)
 | 
				
			||||||
 | 
							font-size 12px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> .thumbnail
 | 
				
			||||||
 | 
								height 80px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> article
 | 
				
			||||||
 | 
								padding 12px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@media (max-width 500px)
 | 
				
			||||||
 | 
							font-size 10px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> .thumbnail
 | 
				
			||||||
 | 
								height 70px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> article
 | 
				
			||||||
 | 
								padding 8px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> header
 | 
				
			||||||
 | 
									margin-bottom 4px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> footer
 | 
				
			||||||
 | 
									margin-top 4px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									> img
 | 
				
			||||||
 | 
										width 12px
 | 
				
			||||||
 | 
										height 12px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										34
									
								
								src/client/app/common/views/pages/page/page.block.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/client/app/common/views/pages/page/page.block.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<component :is="'x-' + value.type" :value="value" :page="page" :script="script" :key="value.id" :h="h"/>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import XText from './page.text.vue';
 | 
				
			||||||
 | 
					import XSection from './page.section.vue';
 | 
				
			||||||
 | 
					import XImage from './page.image.vue';
 | 
				
			||||||
 | 
					import XButton from './page.button.vue';
 | 
				
			||||||
 | 
					import XInput from './page.input.vue';
 | 
				
			||||||
 | 
					import XSwitch from './page.switch.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XText, XSection, XImage, XButton, XInput, XSwitch
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							script: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							page: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							h: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										42
									
								
								src/client/app/common/views/pages/page/page.button.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/client/app/common/views/pages/page/page.button.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,42 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
						<ui-button class="kudkigyw" @click="click()">{{ value.text }}</ui-button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							script: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							click() {
 | 
				
			||||||
 | 
								if (this.value.action === 'dialog') {
 | 
				
			||||||
 | 
									this.script.reEval();
 | 
				
			||||||
 | 
									this.$root.dialog({
 | 
				
			||||||
 | 
										text: this.script.interpolate(this.value.content)
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else if (this.value.action === 'resetRandom') {
 | 
				
			||||||
 | 
									this.script.aiScript.updateRandomSeed(Math.random());
 | 
				
			||||||
 | 
									this.script.reEval();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.kudkigyw
 | 
				
			||||||
 | 
						display inline-block
 | 
				
			||||||
 | 
						min-width 300px
 | 
				
			||||||
 | 
						max-width 450px
 | 
				
			||||||
 | 
						margin 8px 0
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/client/app/common/views/pages/page/page.image.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/client/app/common/views/pages/page/page.image.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div class="lzyxtsnt">
 | 
				
			||||||
 | 
						<img v-if="image" :src="image.url"/>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							page: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								image: null,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							this.image = this.page.attachedFiles.find(x => x.id === this.value.fileId);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.lzyxtsnt
 | 
				
			||||||
 | 
						> img
 | 
				
			||||||
 | 
							max-width 100%
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										43
									
								
								src/client/app/common/views/pages/page/page.input.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/client/app/common/views/pages/page/page.input.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
						<ui-input class="kudkigyw" v-model="v" :type="value.inputType">{{ value.text }}</ui-input>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							script: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								v: this.value.default,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						watch: {
 | 
				
			||||||
 | 
							v() {
 | 
				
			||||||
 | 
								let v = this.v;
 | 
				
			||||||
 | 
								if (this.value.inputType === 'number') v = parseInt(v, 10);
 | 
				
			||||||
 | 
								this.script.aiScript.updatePageVar(this.value.name, v);
 | 
				
			||||||
 | 
								this.script.reEval();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.kudkigyw
 | 
				
			||||||
 | 
						display inline-block
 | 
				
			||||||
 | 
						min-width 300px
 | 
				
			||||||
 | 
						max-width 450px
 | 
				
			||||||
 | 
						margin 8px 0
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										55
									
								
								src/client/app/common/views/pages/page/page.section.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/client/app/common/views/pages/page/page.section.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,55 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<section class="sdgxphyu">
 | 
				
			||||||
 | 
						<component :is="'h' + h">{{ value.title }}</component>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div class="children">
 | 
				
			||||||
 | 
							<x-block v-for="child in value.children" :value="child" :page="page" :script="script" :key="child.id" :h="h + 1"/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							script: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							page: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							h: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						beforeCreate() {
 | 
				
			||||||
 | 
							this.$options.components.XBlock = require('./page.block.vue').default
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.sdgxphyu
 | 
				
			||||||
 | 
						margin 1.5em 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> h2
 | 
				
			||||||
 | 
							font-size 1.35em
 | 
				
			||||||
 | 
							margin 0 0 0.5em 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> h3
 | 
				
			||||||
 | 
							font-size 1em
 | 
				
			||||||
 | 
							margin 0 0 0.5em 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> h4
 | 
				
			||||||
 | 
							font-size 1em
 | 
				
			||||||
 | 
							margin 0 0 0.5em 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> .children
 | 
				
			||||||
 | 
							//padding 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										33
									
								
								src/client/app/common/views/pages/page/page.switch.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/client/app/common/views/pages/page/page.switch.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div>
 | 
				
			||||||
 | 
						<ui-switch v-model="v">{{ value.text }}</ui-switch>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							script: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								v: this.value.default,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						watch: {
 | 
				
			||||||
 | 
							v() {
 | 
				
			||||||
 | 
								this.script.aiScript.updatePageVar(this.value.name, this.v);
 | 
				
			||||||
 | 
								this.script.reEval();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/client/app/common/views/pages/page/page.text.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/client/app/common/views/pages/page/page.text.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div class="">
 | 
				
			||||||
 | 
						<mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							value: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							script: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								text: this.script.interpolate(this.value.text),
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							this.$watch('script.vars', () => {
 | 
				
			||||||
 | 
								this.text = this.script.interpolate(this.value.text);
 | 
				
			||||||
 | 
							}, { deep: true });
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										143
									
								
								src/client/app/common/views/pages/page/page.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/client/app/common/views/pages/page/page.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,143 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div v-if="page" class="iroscrza" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners, center: page.alignCenter }" :style="{ fontFamily: page.font }">
 | 
				
			||||||
 | 
						<header>
 | 
				
			||||||
 | 
							<div class="title">{{ page.title }}</div>
 | 
				
			||||||
 | 
						</header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div v-if="script">
 | 
				
			||||||
 | 
							<x-block v-for="child in page.content" :value="child" @input="v => updateBlock(v)" :page="page" :script="script" :key="child.id" :h="2"/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<footer>
 | 
				
			||||||
 | 
							<small>@{{ page.user.username }}</small>
 | 
				
			||||||
 | 
							<router-link v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId" :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
 | 
				
			||||||
 | 
						</footer>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../../i18n';
 | 
				
			||||||
 | 
					import { faICursor, faPlus, faSquareRootAlt } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import { faSave, faStickyNote } from '@fortawesome/free-regular-svg-icons';
 | 
				
			||||||
 | 
					import XBlock from './page.block.vue';
 | 
				
			||||||
 | 
					import { AiScript } from '../../../scripts/aiscript';
 | 
				
			||||||
 | 
					import { collectPageVars } from '../../../scripts/collect-page-vars';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Script {
 | 
				
			||||||
 | 
						public aiScript: AiScript;
 | 
				
			||||||
 | 
						public vars: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(aiScript) {
 | 
				
			||||||
 | 
							this.aiScript = aiScript;
 | 
				
			||||||
 | 
							this.vars = this.aiScript.evaluateVars();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public reEval() {
 | 
				
			||||||
 | 
							this.vars = this.aiScript.evaluateVars();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public interpolate(str: string) {
 | 
				
			||||||
 | 
							return str.replace(/\{(.+?)\}/g, match =>
 | 
				
			||||||
 | 
								(this.vars.find(x => x.name === match.slice(1, -1).trim()).value || '').toString());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n('pages'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XBlock
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							pageName: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							username: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								page: null,
 | 
				
			||||||
 | 
								script: null,
 | 
				
			||||||
 | 
								faPlus, faICursor, faSave, faStickyNote, faSquareRootAlt
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							this.$root.api('pages/show', {
 | 
				
			||||||
 | 
								name: this.pageName,
 | 
				
			||||||
 | 
								username: this.username,
 | 
				
			||||||
 | 
							}).then(page => {
 | 
				
			||||||
 | 
								this.page = page;
 | 
				
			||||||
 | 
								const pageVars = this.getPageVars();
 | 
				
			||||||
 | 
								this.script = new Script(new AiScript(this.page.variables, pageVars, {
 | 
				
			||||||
 | 
									randomSeed: Math.random(),
 | 
				
			||||||
 | 
									user: page.user,
 | 
				
			||||||
 | 
									visitor: this.$store.state.i
 | 
				
			||||||
 | 
								}));
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							getPageVars() {
 | 
				
			||||||
 | 
								return collectPageVars(this.page.content);
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.iroscrza
 | 
				
			||||||
 | 
						overflow hidden
 | 
				
			||||||
 | 
						background var(--face)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.center
 | 
				
			||||||
 | 
							text-align center
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.round
 | 
				
			||||||
 | 
							border-radius 6px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						&.shadow
 | 
				
			||||||
 | 
							box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> header
 | 
				
			||||||
 | 
							> .title
 | 
				
			||||||
 | 
								z-index 1
 | 
				
			||||||
 | 
								margin 0
 | 
				
			||||||
 | 
								padding 32px 64px
 | 
				
			||||||
 | 
								font-size 24px
 | 
				
			||||||
 | 
								font-weight bold
 | 
				
			||||||
 | 
								color var(--text)
 | 
				
			||||||
 | 
								box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								@media (max-width 600px)
 | 
				
			||||||
 | 
									padding 16px 32px
 | 
				
			||||||
 | 
									font-size 20px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> div
 | 
				
			||||||
 | 
							color var(--text)
 | 
				
			||||||
 | 
							padding 48px 64px
 | 
				
			||||||
 | 
							font-size 18px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@media (max-width 600px)
 | 
				
			||||||
 | 
								padding 24px 32px
 | 
				
			||||||
 | 
								font-size 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> footer
 | 
				
			||||||
 | 
							color var(--text)
 | 
				
			||||||
 | 
							padding 0 64px 38px 64px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@media (max-width 600px)
 | 
				
			||||||
 | 
								padding 0 32px 28px 32px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> small
 | 
				
			||||||
 | 
								display block
 | 
				
			||||||
 | 
								opacity 0.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -156,7 +156,11 @@ init(async (launch, os) => {
 | 
				
			||||||
					{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
 | 
										{ path: '/explore', name: 'explore', component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
 | 
				
			||||||
					{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
 | 
										{ path: '/explore/tags/:tag', name: 'explore-tag', props: true, component: () => import('../common/views/pages/explore.vue').then(m => m.default) },
 | 
				
			||||||
					{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
 | 
										{ path: '/i/favorites', component: () => import('./views/home/favorites.vue').then(m => m.default) },
 | 
				
			||||||
 | 
										{ path: '/i/pages', component: () => import('./views/home/pages.vue').then(m => m.default) },
 | 
				
			||||||
				]},
 | 
									]},
 | 
				
			||||||
 | 
								{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
 | 
				
			||||||
 | 
								{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
 | 
				
			||||||
 | 
								{ path: '/i/pages/edit/:page', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
 | 
				
			||||||
			{ path: '/i/messaging/:user', component: MkMessagingRoom },
 | 
								{ path: '/i/messaging/:user', component: MkMessagingRoom },
 | 
				
			||||||
			{ path: '/i/drive', component: MkDrive },
 | 
								{ path: '/i/drive', component: MkDrive },
 | 
				
			||||||
			{ path: '/i/drive/folder/:folder', component: MkDrive },
 | 
								{ path: '/i/drive/folder/:folder', component: MkDrive },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,35 +9,42 @@
 | 
				
			||||||
			<ul>
 | 
								<ul>
 | 
				
			||||||
				<li>
 | 
									<li>
 | 
				
			||||||
					<router-link :to="`/@${ $store.state.i.username }`">
 | 
										<router-link :to="`/@${ $store.state.i.username }`">
 | 
				
			||||||
						<i><fa icon="user"/></i>
 | 
											<i><fa icon="user" fixed-width/></i>
 | 
				
			||||||
						<span>{{ $t('profile') }}</span>
 | 
											<span>{{ $t('profile') }}</span>
 | 
				
			||||||
						<i><fa icon="angle-right"/></i>
 | 
											<i><fa icon="angle-right"/></i>
 | 
				
			||||||
					</router-link>
 | 
										</router-link>
 | 
				
			||||||
				</li>
 | 
									</li>
 | 
				
			||||||
				<li @click="drive">
 | 
									<li @click="drive">
 | 
				
			||||||
					<p>
 | 
										<p>
 | 
				
			||||||
						<i><fa icon="cloud"/></i>
 | 
											<i><fa icon="cloud" fixed-width/></i>
 | 
				
			||||||
						<span>{{ $t('@.drive') }}</span>
 | 
											<span>{{ $t('@.drive') }}</span>
 | 
				
			||||||
						<i><fa icon="angle-right"/></i>
 | 
											<i><fa icon="angle-right"/></i>
 | 
				
			||||||
					</p>
 | 
										</p>
 | 
				
			||||||
				</li>
 | 
									</li>
 | 
				
			||||||
				<li>
 | 
									<li>
 | 
				
			||||||
					<router-link to="/i/favorites">
 | 
										<router-link to="/i/favorites">
 | 
				
			||||||
						<i><fa icon="star"/></i>
 | 
											<i><fa icon="star" fixed-width/></i>
 | 
				
			||||||
						<span>{{ $t('@.favorites') }}</span>
 | 
											<span>{{ $t('@.favorites') }}</span>
 | 
				
			||||||
						<i><fa icon="angle-right"/></i>
 | 
											<i><fa icon="angle-right"/></i>
 | 
				
			||||||
					</router-link>
 | 
										</router-link>
 | 
				
			||||||
				</li>
 | 
									</li>
 | 
				
			||||||
				<li @click="list">
 | 
									<li @click="list">
 | 
				
			||||||
					<p>
 | 
										<p>
 | 
				
			||||||
						<i><fa icon="list"/></i>
 | 
											<i><fa icon="list" fixed-width/></i>
 | 
				
			||||||
						<span>{{ $t('lists') }}</span>
 | 
											<span>{{ $t('lists') }}</span>
 | 
				
			||||||
						<i><fa icon="angle-right"/></i>
 | 
											<i><fa icon="angle-right"/></i>
 | 
				
			||||||
					</p>
 | 
										</p>
 | 
				
			||||||
				</li>
 | 
									</li>
 | 
				
			||||||
 | 
									<li @click="page">
 | 
				
			||||||
 | 
										<router-link to="/i/pages">
 | 
				
			||||||
 | 
											<i><fa :icon="faStickyNote" fixed-width/></i>
 | 
				
			||||||
 | 
											<span>{{ $t('@.pages') }}</span>
 | 
				
			||||||
 | 
											<i><fa icon="angle-right"/></i>
 | 
				
			||||||
 | 
										</router-link>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
				<li @click="followRequests" v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
 | 
									<li @click="followRequests" v-if="($store.state.i.isLocked || $store.state.i.carefulBot)">
 | 
				
			||||||
					<p>
 | 
										<p>
 | 
				
			||||||
						<i><fa :icon="['far', 'envelope']"/></i>
 | 
											<i><fa :icon="['far', 'envelope']" fixed-width/></i>
 | 
				
			||||||
						<span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>
 | 
											<span>{{ $t('follow-requests') }}<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>
 | 
				
			||||||
						<i><fa icon="angle-right"/></i>
 | 
											<i><fa icon="angle-right"/></i>
 | 
				
			||||||
					</p>
 | 
										</p>
 | 
				
			||||||
| 
						 | 
					@ -46,14 +53,14 @@
 | 
				
			||||||
			<ul>
 | 
								<ul>
 | 
				
			||||||
				<li>
 | 
									<li>
 | 
				
			||||||
					<router-link to="/i/settings">
 | 
										<router-link to="/i/settings">
 | 
				
			||||||
						<i><fa icon="cog"/></i>
 | 
											<i><fa icon="cog" fixed-width/></i>
 | 
				
			||||||
						<span>{{ $t('@.settings') }}</span>
 | 
											<span>{{ $t('@.settings') }}</span>
 | 
				
			||||||
						<i><fa icon="angle-right"/></i>
 | 
											<i><fa icon="angle-right"/></i>
 | 
				
			||||||
					</router-link>
 | 
										</router-link>
 | 
				
			||||||
				</li>
 | 
									</li>
 | 
				
			||||||
				<li v-if="$store.state.i.isAdmin || $store.state.i.isModerator">
 | 
									<li v-if="$store.state.i.isAdmin || $store.state.i.isModerator">
 | 
				
			||||||
					<a href="/admin">
 | 
										<a href="/admin">
 | 
				
			||||||
						<i><fa icon="terminal"/></i>
 | 
											<i><fa icon="terminal" fixed-width/></i>
 | 
				
			||||||
						<span>{{ $t('admin') }}</span>
 | 
											<span>{{ $t('admin') }}</span>
 | 
				
			||||||
						<i><fa icon="angle-right"/></i>
 | 
											<i><fa icon="angle-right"/></i>
 | 
				
			||||||
					</a>
 | 
										</a>
 | 
				
			||||||
| 
						 | 
					@ -76,7 +83,7 @@
 | 
				
			||||||
			<ul>
 | 
								<ul>
 | 
				
			||||||
				<li @click="signout">
 | 
									<li @click="signout">
 | 
				
			||||||
					<p class="signout">
 | 
										<p class="signout">
 | 
				
			||||||
						<i><fa icon="power-off"/></i>
 | 
											<i><fa icon="power-off" fixed-width/></i>
 | 
				
			||||||
						<span>{{ $t('@.signout') }}</span>
 | 
											<span>{{ $t('@.signout') }}</span>
 | 
				
			||||||
					</p>
 | 
										</p>
 | 
				
			||||||
				</li>
 | 
									</li>
 | 
				
			||||||
| 
						 | 
					@ -95,14 +102,14 @@ import MkFollowRequestsWindow from './received-follow-requests-window.vue';
 | 
				
			||||||
import MkDriveWindow from './drive-window.vue';
 | 
					import MkDriveWindow from './drive-window.vue';
 | 
				
			||||||
import contains from '../../../common/scripts/contains';
 | 
					import contains from '../../../common/scripts/contains';
 | 
				
			||||||
import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
 | 
					import { faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
 | 
					import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Vue.extend({
 | 
					export default Vue.extend({
 | 
				
			||||||
	i18n: i18n('desktop/views/components/ui.header.account.vue'),
 | 
						i18n: i18n('desktop/views/components/ui.header.account.vue'),
 | 
				
			||||||
	data() {
 | 
						data() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			isOpen: false,
 | 
								isOpen: false,
 | 
				
			||||||
			faHome, faColumns, faMoon, faSun
 | 
								faHome, faColumns, faMoon, faSun, faStickyNote
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	computed: {
 | 
						computed: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										92
									
								
								src/client/app/desktop/views/home/pages.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/client/app/desktop/views/home/pages.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,92 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div class="rknalgpo" v-if="!fetching">
 | 
				
			||||||
 | 
						<ui-button @click="create()"><fa :icon="faPlus"/></ui-button>
 | 
				
			||||||
 | 
						<sequential-entrance animation="entranceFromTop" delay="25">
 | 
				
			||||||
 | 
							<template v-for="page in pages">
 | 
				
			||||||
 | 
								<x-page-preview class="page" :page="page" :key="page.id"/>
 | 
				
			||||||
 | 
							</template>
 | 
				
			||||||
 | 
						</sequential-entrance>
 | 
				
			||||||
 | 
						<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../i18n';
 | 
				
			||||||
 | 
					import Progress from '../../../common/scripts/loading';
 | 
				
			||||||
 | 
					import { faPlus } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
 | 
				
			||||||
 | 
					import XPagePreview from '../../../common/views/components/page-preview.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n(),
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XPagePreview
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								fetching: true,
 | 
				
			||||||
 | 
								pages: [],
 | 
				
			||||||
 | 
								existMore: false,
 | 
				
			||||||
 | 
								moreFetching: false,
 | 
				
			||||||
 | 
								faStickyNote, faPlus
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							this.fetch();
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							fetch() {
 | 
				
			||||||
 | 
								Progress.start();
 | 
				
			||||||
 | 
								this.fetching = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this.$root.api('i/pages', {
 | 
				
			||||||
 | 
									limit: 11
 | 
				
			||||||
 | 
								}).then(pages => {
 | 
				
			||||||
 | 
									if (pages.length == 11) {
 | 
				
			||||||
 | 
										this.existMore = true;
 | 
				
			||||||
 | 
										pages.pop();
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									this.pages = pages;
 | 
				
			||||||
 | 
									this.fetching = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									Progress.done();
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							fetchMore() {
 | 
				
			||||||
 | 
								this.moreFetching = true;
 | 
				
			||||||
 | 
								this.$root.api('i/pages', {
 | 
				
			||||||
 | 
									limit: 11,
 | 
				
			||||||
 | 
									untilId: this.pages[this.pages.length - 1].id
 | 
				
			||||||
 | 
								}).then(pages => {
 | 
				
			||||||
 | 
									if (pages.length == 11) {
 | 
				
			||||||
 | 
										this.existMore = true;
 | 
				
			||||||
 | 
										pages.pop();
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										this.existMore = false;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									this.pages = this.pages.concat(pages);
 | 
				
			||||||
 | 
									this.moreFetching = false;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							create() {
 | 
				
			||||||
 | 
								this.$router.push(`/i/pages/new`);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					.rknalgpo
 | 
				
			||||||
 | 
						margin 0 auto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> * > .page
 | 
				
			||||||
 | 
							margin-bottom 8px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@media (min-width 500px)
 | 
				
			||||||
 | 
							> * > .page
 | 
				
			||||||
 | 
								margin-bottom 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/client/app/desktop/views/pages/page-editor.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/client/app/desktop/views/pages/page-editor.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<mk-ui>
 | 
				
			||||||
 | 
						<main>
 | 
				
			||||||
 | 
							<x-page-editor :page="page"/>
 | 
				
			||||||
 | 
						</main>
 | 
				
			||||||
 | 
					</mk-ui>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XPageEditor: () => import('../../../common/views/components/page-editor/page-editor.vue').then(m => m.default)
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							page: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					main
 | 
				
			||||||
 | 
						margin 0 auto
 | 
				
			||||||
 | 
						padding 16px
 | 
				
			||||||
 | 
						max-width 900px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/client/app/desktop/views/pages/page.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/client/app/desktop/views/pages/page.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<mk-ui>
 | 
				
			||||||
 | 
						<main>
 | 
				
			||||||
 | 
							<x-page :page-name="page" :username="user"/>
 | 
				
			||||||
 | 
						</main>
 | 
				
			||||||
 | 
					</mk-ui>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XPage: () => import('../../../common/views/pages/page/page.vue').then(m => m.default)
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							page: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							user: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					main
 | 
				
			||||||
 | 
						margin 0 auto
 | 
				
			||||||
 | 
						padding 16px
 | 
				
			||||||
 | 
						max-width 950px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -135,6 +135,7 @@ init((launch, os) => {
 | 
				
			||||||
			{ path: '/signup', name: 'signup', component: MkSignup },
 | 
								{ path: '/signup', name: 'signup', component: MkSignup },
 | 
				
			||||||
			{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) },
 | 
								{ path: '/i/settings', name: 'settings', component: () => import('./views/pages/settings.vue').then(m => m.default) },
 | 
				
			||||||
			{ path: '/i/favorites', name: 'favorites', component: MkFavorites },
 | 
								{ path: '/i/favorites', name: 'favorites', component: MkFavorites },
 | 
				
			||||||
 | 
								{ path: '/i/pages', name: 'pages', component: () => import('./views/pages/pages.vue').then(m => m.default) },
 | 
				
			||||||
			{ path: '/i/lists', name: 'user-lists', component: MkUserLists },
 | 
								{ path: '/i/lists', name: 'user-lists', component: MkUserLists },
 | 
				
			||||||
			{ path: '/i/lists/:list', name: 'user-list', component: MkUserList },
 | 
								{ path: '/i/lists/:list', name: 'user-list', component: MkUserList },
 | 
				
			||||||
			{ path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests },
 | 
								{ path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests },
 | 
				
			||||||
| 
						 | 
					@ -144,6 +145,8 @@ init((launch, os) => {
 | 
				
			||||||
			{ path: '/i/drive', name: 'drive', component: MkDrive },
 | 
								{ path: '/i/drive', name: 'drive', component: MkDrive },
 | 
				
			||||||
			{ path: '/i/drive/folder/:folder', component: MkDrive },
 | 
								{ path: '/i/drive/folder/:folder', component: MkDrive },
 | 
				
			||||||
			{ path: '/i/drive/file/:file', component: MkDrive },
 | 
								{ path: '/i/drive/file/:file', component: MkDrive },
 | 
				
			||||||
 | 
								{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
 | 
				
			||||||
 | 
								{ path: '/i/pages/edit/:page', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
 | 
				
			||||||
			{ path: '/selectdrive', component: MkSelectDrive },
 | 
								{ path: '/selectdrive', component: MkSelectDrive },
 | 
				
			||||||
			{ path: '/search', component: MkSearch },
 | 
								{ path: '/search', component: MkSearch },
 | 
				
			||||||
			{ path: '/tags/:tag', component: MkTag },
 | 
								{ path: '/tags/:tag', component: MkTag },
 | 
				
			||||||
| 
						 | 
					@ -156,6 +159,7 @@ init((launch, os) => {
 | 
				
			||||||
				{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
 | 
									{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
 | 
				
			||||||
				{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
 | 
									{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
 | 
				
			||||||
			]},
 | 
								]},
 | 
				
			||||||
 | 
								{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
 | 
				
			||||||
			{ path: '/notes/:note', component: MkNote },
 | 
								{ path: '/notes/:note', component: MkNote },
 | 
				
			||||||
			{ path: '/authorize-follow', component: MkFollow },
 | 
								{ path: '/authorize-follow', component: MkFollow },
 | 
				
			||||||
			{ path: '*', component: MkNotFound }
 | 
								{ path: '*', component: MkNotFound }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@
 | 
				
			||||||
						<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('@.favorites') }}<i><fa icon="angle-right"/></i></router-link></li>
 | 
											<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('@.favorites') }}<i><fa icon="angle-right"/></i></router-link></li>
 | 
				
			||||||
						<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li>
 | 
											<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li>
 | 
				
			||||||
						<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
 | 
											<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
 | 
				
			||||||
 | 
											<li><router-link to="/i/pages" :data-active="$route.name == 'pages'"><i><fa :icon="faStickyNote" fixed-width/></i>{{ $t('@.pages') }}<i><fa icon="angle-right"/></i></router-link></li>
 | 
				
			||||||
					</ul>
 | 
										</ul>
 | 
				
			||||||
					<ul>
 | 
										<ul>
 | 
				
			||||||
						<li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li>
 | 
											<li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li>
 | 
				
			||||||
| 
						 | 
					@ -66,7 +67,7 @@ import Vue from 'vue';
 | 
				
			||||||
import i18n from '../../../i18n';
 | 
					import i18n from '../../../i18n';
 | 
				
			||||||
import { lang } from '../../../config';
 | 
					import { lang } from '../../../config';
 | 
				
			||||||
import { faNewspaper, faHashtag, faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
 | 
					import { faNewspaper, faHashtag, faHome, faColumns } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
 | 
					import { faMoon, faSun, faStickyNote } from '@fortawesome/free-regular-svg-icons';
 | 
				
			||||||
import { search } from '../../../common/scripts/search';
 | 
					import { search } from '../../../common/scripts/search';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Vue.extend({
 | 
					export default Vue.extend({
 | 
				
			||||||
| 
						 | 
					@ -86,7 +87,7 @@ export default Vue.extend({
 | 
				
			||||||
			announcements: [],
 | 
								announcements: [],
 | 
				
			||||||
			searching: false,
 | 
								searching: false,
 | 
				
			||||||
			showNotifications: false,
 | 
								showNotifications: false,
 | 
				
			||||||
			faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns
 | 
								faNewspaper, faHashtag, faMoon, faSun, faHome, faColumns, faStickyNote
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/client/app/mobile/views/pages/page-editor.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/client/app/mobile/views/pages/page-editor.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<mk-ui>
 | 
				
			||||||
 | 
						<main>
 | 
				
			||||||
 | 
							<x-page-editor :page="page"/>
 | 
				
			||||||
 | 
						</main>
 | 
				
			||||||
 | 
					</mk-ui>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XPageEditor: () => import('../../../common/views/components/page-editor/page-editor.vue').then(m => m.default)
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							page: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					main
 | 
				
			||||||
 | 
						margin 0 auto
 | 
				
			||||||
 | 
						padding 16px
 | 
				
			||||||
 | 
						max-width 1000px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/client/app/mobile/views/pages/page.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/client/app/mobile/views/pages/page.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<mk-ui>
 | 
				
			||||||
 | 
						<main>
 | 
				
			||||||
 | 
							<x-page :page-name="page" :username="user"/>
 | 
				
			||||||
 | 
						</main>
 | 
				
			||||||
 | 
					</mk-ui>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XPage: () => import('../../../common/views/pages/page/page.vue').then(m => m.default)
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							page: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							user: {
 | 
				
			||||||
 | 
								type: String,
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					main
 | 
				
			||||||
 | 
						margin 0 auto
 | 
				
			||||||
 | 
						padding 16px
 | 
				
			||||||
 | 
						max-width 1000px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										94
									
								
								src/client/app/mobile/views/pages/pages.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/client/app/mobile/views/pages/pages.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,94 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<mk-ui>
 | 
				
			||||||
 | 
						<template #header><span style="margin-right:4px;"><fa :icon="faStickyNote"/></span>{{ $t('@.pages') }}</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<main>
 | 
				
			||||||
 | 
							<ui-button @click="create()"><fa :icon="faPlus"/></ui-button>
 | 
				
			||||||
 | 
							<sequential-entrance animation="entranceFromTop" delay="25">
 | 
				
			||||||
 | 
								<template v-for="page in pages">
 | 
				
			||||||
 | 
									<x-page-preview class="page" :page="page" :key="page.id"/>
 | 
				
			||||||
 | 
								</template>
 | 
				
			||||||
 | 
							</sequential-entrance>
 | 
				
			||||||
 | 
							<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
 | 
				
			||||||
 | 
						</main>
 | 
				
			||||||
 | 
					</mk-ui>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import i18n from '../../../i18n';
 | 
				
			||||||
 | 
					import Progress from '../../../common/scripts/loading';
 | 
				
			||||||
 | 
					import { faPlus } from '@fortawesome/free-solid-svg-icons';
 | 
				
			||||||
 | 
					import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
 | 
				
			||||||
 | 
					import XPagePreview from '../../../common/views/components/page-preview.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						i18n: i18n(),
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XPagePreview
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								fetching: true,
 | 
				
			||||||
 | 
								pages: [],
 | 
				
			||||||
 | 
								existMore: false,
 | 
				
			||||||
 | 
								moreFetching: false,
 | 
				
			||||||
 | 
								faStickyNote, faPlus
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							this.fetch();
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							fetch() {
 | 
				
			||||||
 | 
								Progress.start();
 | 
				
			||||||
 | 
								this.fetching = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this.$root.api('i/pages', {
 | 
				
			||||||
 | 
									limit: 11
 | 
				
			||||||
 | 
								}).then(pages => {
 | 
				
			||||||
 | 
									if (pages.length == 11) {
 | 
				
			||||||
 | 
										this.existMore = true;
 | 
				
			||||||
 | 
										pages.pop();
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									this.pages = pages;
 | 
				
			||||||
 | 
									this.fetching = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									Progress.done();
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							fetchMore() {
 | 
				
			||||||
 | 
								this.moreFetching = true;
 | 
				
			||||||
 | 
								this.$root.api('i/pages', {
 | 
				
			||||||
 | 
									limit: 11,
 | 
				
			||||||
 | 
									untilId: this.pages[this.pages.length - 1].id
 | 
				
			||||||
 | 
								}).then(pages => {
 | 
				
			||||||
 | 
									if (pages.length == 11) {
 | 
				
			||||||
 | 
										this.existMore = true;
 | 
				
			||||||
 | 
										pages.pop();
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										this.existMore = false;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									this.pages = this.pages.concat(pages);
 | 
				
			||||||
 | 
									this.moreFetching = false;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							create() {
 | 
				
			||||||
 | 
								this.$router.push(`/i/pages/new`);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					main
 | 
				
			||||||
 | 
						> * > .page
 | 
				
			||||||
 | 
							margin-bottom 8px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@media (min-width 500px)
 | 
				
			||||||
 | 
							> * > .page
 | 
				
			||||||
 | 
								margin-bottom 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -232,5 +232,8 @@
 | 
				
			||||||
		adminDashboardCardBg: '$secondary',
 | 
							adminDashboardCardBg: '$secondary',
 | 
				
			||||||
		adminDashboardCardFg: '$text',
 | 
							adminDashboardCardFg: '$text',
 | 
				
			||||||
		adminDashboardCardDivider: 'rgba(0, 0, 0, 0.3)',
 | 
							adminDashboardCardDivider: 'rgba(0, 0, 0, 0.3)',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							pageBlockBorder: 'rgba(255, 255, 255, 0.1)',
 | 
				
			||||||
 | 
							pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)',
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -232,5 +232,8 @@
 | 
				
			||||||
		adminDashboardCardBg: '$secondary',
 | 
							adminDashboardCardBg: '$secondary',
 | 
				
			||||||
		adminDashboardCardFg: '$text',
 | 
							adminDashboardCardFg: '$text',
 | 
				
			||||||
		adminDashboardCardDivider: 'rgba(0, 0, 0, 0.082)',
 | 
							adminDashboardCardDivider: 'rgba(0, 0, 0, 0.082)',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							pageBlockBorder: 'rgba(0, 0, 0, 0.1)',
 | 
				
			||||||
 | 
							pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)',
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,6 +40,7 @@ import { Poll } from '../models/entities/poll';
 | 
				
			||||||
import { UserKeypair } from '../models/entities/user-keypair';
 | 
					import { UserKeypair } from '../models/entities/user-keypair';
 | 
				
			||||||
import { UserPublickey } from '../models/entities/user-publickey';
 | 
					import { UserPublickey } from '../models/entities/user-publickey';
 | 
				
			||||||
import { UserProfile } from '../models/entities/user-profile';
 | 
					import { UserProfile } from '../models/entities/user-profile';
 | 
				
			||||||
 | 
					import { Page } from '../models/entities/page';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
 | 
					const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -114,6 +115,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
 | 
				
			||||||
			NoteReaction,
 | 
								NoteReaction,
 | 
				
			||||||
			NoteWatching,
 | 
								NoteWatching,
 | 
				
			||||||
			NoteUnread,
 | 
								NoteUnread,
 | 
				
			||||||
 | 
								Page,
 | 
				
			||||||
			Log,
 | 
								Log,
 | 
				
			||||||
			DriveFile,
 | 
								DriveFile,
 | 
				
			||||||
			DriveFolder,
 | 
								DriveFolder,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										105
									
								
								src/models/entities/page.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/models/entities/page.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,105 @@
 | 
				
			||||||
 | 
					import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
 | 
				
			||||||
 | 
					import { User } from './user';
 | 
				
			||||||
 | 
					import { id } from '../id';
 | 
				
			||||||
 | 
					import { DriveFile } from './drive-file';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity()
 | 
				
			||||||
 | 
					@Index(['userId', 'name'], { unique: true })
 | 
				
			||||||
 | 
					export class Page {
 | 
				
			||||||
 | 
						@PrimaryColumn(id())
 | 
				
			||||||
 | 
						public id: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Index()
 | 
				
			||||||
 | 
						@Column('timestamp with time zone', {
 | 
				
			||||||
 | 
							comment: 'The created date of the Page.'
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public createdAt: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Index()
 | 
				
			||||||
 | 
						@Column('timestamp with time zone', {
 | 
				
			||||||
 | 
							comment: 'The updated date of the Page.'
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public updatedAt: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column('varchar', {
 | 
				
			||||||
 | 
							length: 256,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public title: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Index()
 | 
				
			||||||
 | 
						@Column('varchar', {
 | 
				
			||||||
 | 
							length: 256,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public name: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column('varchar', {
 | 
				
			||||||
 | 
							length: 256, nullable: true
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public summary: string | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column('boolean')
 | 
				
			||||||
 | 
						public alignCenter: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column('varchar', {
 | 
				
			||||||
 | 
							length: 32,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public font: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Index()
 | 
				
			||||||
 | 
						@Column({
 | 
				
			||||||
 | 
							...id(),
 | 
				
			||||||
 | 
							comment: 'The ID of author.'
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public userId: User['id'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@ManyToOne(type => User, {
 | 
				
			||||||
 | 
							onDelete: 'CASCADE'
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						@JoinColumn()
 | 
				
			||||||
 | 
						public user: User | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column({
 | 
				
			||||||
 | 
							...id(),
 | 
				
			||||||
 | 
							nullable: true,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public eyeCatchingImageId: DriveFile['id'] | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@ManyToOne(type => DriveFile, {
 | 
				
			||||||
 | 
							onDelete: 'CASCADE'
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						@JoinColumn()
 | 
				
			||||||
 | 
						public eyeCatchingImage: DriveFile | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column('jsonb', {
 | 
				
			||||||
 | 
							default: []
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public content: Record<string, any>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column('jsonb', {
 | 
				
			||||||
 | 
							default: []
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public variables: Record<string, any>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * public ... 公開
 | 
				
			||||||
 | 
						 * followers ... フォロワーのみ
 | 
				
			||||||
 | 
						 * specified ... visibleUserIds で指定したユーザーのみ
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@Column('enum', { enum: ['public', 'followers', 'specified'] })
 | 
				
			||||||
 | 
						public visibility: 'public' | 'followers' | 'specified';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Index()
 | 
				
			||||||
 | 
						@Column({
 | 
				
			||||||
 | 
							...id(),
 | 
				
			||||||
 | 
							array: true, default: '{}'
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public visibleUserIds: User['id'][];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(data: Partial<Page>) {
 | 
				
			||||||
 | 
							if (data == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (const [k, v] of Object.entries(data)) {
 | 
				
			||||||
 | 
								(this as any)[k] = v;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -35,6 +35,7 @@ import { AbuseUserReportRepository } from './repositories/abuse-user-report';
 | 
				
			||||||
import { AuthSessionRepository } from './repositories/auth-session';
 | 
					import { AuthSessionRepository } from './repositories/auth-session';
 | 
				
			||||||
import { UserProfile } from './entities/user-profile';
 | 
					import { UserProfile } from './entities/user-profile';
 | 
				
			||||||
import { HashtagRepository } from './repositories/hashtag';
 | 
					import { HashtagRepository } from './repositories/hashtag';
 | 
				
			||||||
 | 
					import { PageRepository } from './repositories/page';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Apps = getCustomRepository(AppRepository);
 | 
					export const Apps = getCustomRepository(AppRepository);
 | 
				
			||||||
export const Notes = getCustomRepository(NoteRepository);
 | 
					export const Notes = getCustomRepository(NoteRepository);
 | 
				
			||||||
| 
						 | 
					@ -72,3 +73,4 @@ export const MessagingMessages = getCustomRepository(MessagingMessageRepository)
 | 
				
			||||||
export const ReversiGames = getCustomRepository(ReversiGameRepository);
 | 
					export const ReversiGames = getCustomRepository(ReversiGameRepository);
 | 
				
			||||||
export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
 | 
					export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
 | 
				
			||||||
export const Logs = getRepository(Log);
 | 
					export const Logs = getRepository(Log);
 | 
				
			||||||
 | 
					export const Pages = getCustomRepository(PageRepository);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										61
									
								
								src/models/repositories/page.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/models/repositories/page.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,61 @@
 | 
				
			||||||
 | 
					import { EntityRepository, Repository } from 'typeorm';
 | 
				
			||||||
 | 
					import { Page } from '../entities/page';
 | 
				
			||||||
 | 
					import { SchemaType, types, bool } from '../../misc/schema';
 | 
				
			||||||
 | 
					import { Users, DriveFiles } from '..';
 | 
				
			||||||
 | 
					import { awaitAll } from '../../prelude/await-all';
 | 
				
			||||||
 | 
					import { DriveFile } from '../entities/drive-file';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type PackedPage = SchemaType<typeof packedPageSchema>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@EntityRepository(Page)
 | 
				
			||||||
 | 
					export class PageRepository extends Repository<Page> {
 | 
				
			||||||
 | 
						public async pack(
 | 
				
			||||||
 | 
							src: Page,
 | 
				
			||||||
 | 
						): Promise<PackedPage> {
 | 
				
			||||||
 | 
							const attachedFiles: Promise<DriveFile | undefined>[] = [];
 | 
				
			||||||
 | 
							const collectFile = (xs: any[]) => {
 | 
				
			||||||
 | 
								for (const x of xs) {
 | 
				
			||||||
 | 
									if (x.type === 'image') {
 | 
				
			||||||
 | 
										attachedFiles.push(DriveFiles.findOne({
 | 
				
			||||||
 | 
											id: x.fileId,
 | 
				
			||||||
 | 
											userId: src.userId
 | 
				
			||||||
 | 
										}));
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if (x.children) {
 | 
				
			||||||
 | 
										collectFile(x.children);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
							collectFile(src.content);
 | 
				
			||||||
 | 
							return await awaitAll({
 | 
				
			||||||
 | 
								id: src.id,
 | 
				
			||||||
 | 
								createdAt: src.createdAt.toISOString(),
 | 
				
			||||||
 | 
								updatedAt: src.updatedAt.toISOString(),
 | 
				
			||||||
 | 
								userId: src.userId,
 | 
				
			||||||
 | 
								user: Users.pack(src.user || src.userId),
 | 
				
			||||||
 | 
								content: src.content,
 | 
				
			||||||
 | 
								variables: src.variables,
 | 
				
			||||||
 | 
								title: src.title,
 | 
				
			||||||
 | 
								name: src.name,
 | 
				
			||||||
 | 
								summary: src.summary,
 | 
				
			||||||
 | 
								alignCenter: src.alignCenter,
 | 
				
			||||||
 | 
								font: src.font,
 | 
				
			||||||
 | 
								eyeCatchingImageId: src.eyeCatchingImageId,
 | 
				
			||||||
 | 
								eyeCatchingImage: src.eyeCatchingImageId ? await DriveFiles.pack(src.eyeCatchingImageId) : null,
 | 
				
			||||||
 | 
								attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles))
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public packMany(
 | 
				
			||||||
 | 
							pages: Page[],
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							return Promise.all(pages.map(x => this.pack(x)));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const packedPageSchema = {
 | 
				
			||||||
 | 
						type: types.object,
 | 
				
			||||||
 | 
						optional: bool.false, nullable: bool.false,
 | 
				
			||||||
 | 
						properties: {
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										44
									
								
								src/server/api/endpoints/i/pages.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/server/api/endpoints/i/pages.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import { ID } from '../../../../misc/cafy-id';
 | 
				
			||||||
 | 
					import define from '../../define';
 | 
				
			||||||
 | 
					import { Pages } from '../../../../models';
 | 
				
			||||||
 | 
					import { makePaginationQuery } from '../../common/make-pagination-query';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						desc: {
 | 
				
			||||||
 | 
							'ja-JP': '自分の作成したページ一覧を取得します。',
 | 
				
			||||||
 | 
							'en-US': 'Get my pages.'
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tags: ['account', 'pages'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						kind: 'read:pages',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							limit: {
 | 
				
			||||||
 | 
								validator: $.optional.num.range(1, 100),
 | 
				
			||||||
 | 
								default: 10
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							sinceId: {
 | 
				
			||||||
 | 
								validator: $.optional.type(ID),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							untilId: {
 | 
				
			||||||
 | 
								validator: $.optional.type(ID),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default define(meta, async (ps, user) => {
 | 
				
			||||||
 | 
						const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
 | 
				
			||||||
 | 
							.andWhere(`page.userId = :meId`, { meId: user.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const pages = await query
 | 
				
			||||||
 | 
							.take(ps.limit!)
 | 
				
			||||||
 | 
							.getMany();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return await Pages.packMany(pages);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										108
									
								
								src/server/api/endpoints/pages/create.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/server/api/endpoints/pages/create.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,108 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import * as ms from 'ms';
 | 
				
			||||||
 | 
					import define from '../../define';
 | 
				
			||||||
 | 
					import { ID } from '../../../../misc/cafy-id';
 | 
				
			||||||
 | 
					import { types, bool } from '../../../../misc/schema';
 | 
				
			||||||
 | 
					import { Pages, DriveFiles } from '../../../../models';
 | 
				
			||||||
 | 
					import { genId } from '../../../../misc/gen-id';
 | 
				
			||||||
 | 
					import { Page } from '../../../../models/entities/page';
 | 
				
			||||||
 | 
					import { ApiError } from '../../error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						desc: {
 | 
				
			||||||
 | 
							'ja-JP': 'ページを作成します。',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tags: ['pages'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						kind: 'write:pages',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						limit: {
 | 
				
			||||||
 | 
							duration: ms('1hour'),
 | 
				
			||||||
 | 
							max: 300
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							title: {
 | 
				
			||||||
 | 
								validator: $.str,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							name: {
 | 
				
			||||||
 | 
								validator: $.str,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							summary: {
 | 
				
			||||||
 | 
								validator: $.optional.nullable.str,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							content: {
 | 
				
			||||||
 | 
								validator: $.arr($.obj())
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							variables: {
 | 
				
			||||||
 | 
								validator: $.arr($.obj())
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							eyeCatchingImageId: {
 | 
				
			||||||
 | 
								validator: $.optional.nullable.type(ID),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							font: {
 | 
				
			||||||
 | 
								validator: $.optional.str.or(['serif', 'sans-serif']),
 | 
				
			||||||
 | 
								default: 'sans-serif'
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							alignCenter: {
 | 
				
			||||||
 | 
								validator: $.optional.bool,
 | 
				
			||||||
 | 
								default: false
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res: {
 | 
				
			||||||
 | 
							type: types.object,
 | 
				
			||||||
 | 
							optional: bool.false, nullable: bool.false,
 | 
				
			||||||
 | 
							ref: 'Page',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						errors: {
 | 
				
			||||||
 | 
							noSuchFile: {
 | 
				
			||||||
 | 
								message: 'No such file.',
 | 
				
			||||||
 | 
								code: 'NO_SUCH_FILE',
 | 
				
			||||||
 | 
								id: 'b7b97489-0f66-4b12-a5ff-b21bd63f6e1c'
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default define(meta, async (ps, user) => {
 | 
				
			||||||
 | 
						let eyeCatchingImage = null;
 | 
				
			||||||
 | 
						if (ps.eyeCatchingImageId != null) {
 | 
				
			||||||
 | 
							eyeCatchingImage = await DriveFiles.findOne({
 | 
				
			||||||
 | 
								id: ps.eyeCatchingImageId,
 | 
				
			||||||
 | 
								userId: user.id
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (eyeCatchingImage == null) {
 | 
				
			||||||
 | 
								throw new ApiError(meta.errors.noSuchFile);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const page = await Pages.save(new Page({
 | 
				
			||||||
 | 
							id: genId(),
 | 
				
			||||||
 | 
							createdAt: new Date(),
 | 
				
			||||||
 | 
							updatedAt: new Date(),
 | 
				
			||||||
 | 
							title: ps.title,
 | 
				
			||||||
 | 
							name: ps.name,
 | 
				
			||||||
 | 
							summary: ps.summary,
 | 
				
			||||||
 | 
							content: ps.content,
 | 
				
			||||||
 | 
							variables: ps.variables,
 | 
				
			||||||
 | 
							eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null,
 | 
				
			||||||
 | 
							userId: user.id,
 | 
				
			||||||
 | 
							visibility: 'public',
 | 
				
			||||||
 | 
							alignCenter: ps.alignCenter,
 | 
				
			||||||
 | 
							font: ps.font
 | 
				
			||||||
 | 
						}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return await Pages.pack(page);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										53
									
								
								src/server/api/endpoints/pages/delete.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/server/api/endpoints/pages/delete.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import define from '../../define';
 | 
				
			||||||
 | 
					import { ApiError } from '../../error';
 | 
				
			||||||
 | 
					import { Pages } from '../../../../models';
 | 
				
			||||||
 | 
					import { ID } from '../../../../misc/cafy-id';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						desc: {
 | 
				
			||||||
 | 
							'ja-JP': '指定したページを削除します。',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tags: ['pages'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						kind: 'write:pages',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							pageId: {
 | 
				
			||||||
 | 
								validator: $.type(ID),
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									'ja-JP': '対象のページのID',
 | 
				
			||||||
 | 
									'en-US': 'Target page ID.'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						errors: {
 | 
				
			||||||
 | 
							noSuchPage: {
 | 
				
			||||||
 | 
								message: 'No such page.',
 | 
				
			||||||
 | 
								code: 'NO_SUCH_PAGE',
 | 
				
			||||||
 | 
								id: 'eb0c6e1d-d519-4764-9486-52a7e1c6392a'
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							accessDenied: {
 | 
				
			||||||
 | 
								message: 'Access denied.',
 | 
				
			||||||
 | 
								code: 'ACCESS_DENIED',
 | 
				
			||||||
 | 
								id: '8b741b3e-2c22-44b3-a15f-29949aa1601e'
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default define(meta, async (ps, user) => {
 | 
				
			||||||
 | 
						const page = await Pages.findOne(ps.pageId);
 | 
				
			||||||
 | 
						if (page == null) {
 | 
				
			||||||
 | 
							throw new ApiError(meta.errors.noSuchPage);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (page.userId !== user.id) {
 | 
				
			||||||
 | 
							throw new ApiError(meta.errors.accessDenied);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await Pages.delete(page.id);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										74
									
								
								src/server/api/endpoints/pages/show.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/server/api/endpoints/pages/show.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,74 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import define from '../../define';
 | 
				
			||||||
 | 
					import { ApiError } from '../../error';
 | 
				
			||||||
 | 
					import { Pages, Users } from '../../../../models';
 | 
				
			||||||
 | 
					import { types, bool } from '../../../../misc/schema';
 | 
				
			||||||
 | 
					import { ID } from '../../../../misc/cafy-id';
 | 
				
			||||||
 | 
					import { Page } from '../../../../models/entities/page';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						desc: {
 | 
				
			||||||
 | 
							'ja-JP': '指定したページの情報を取得します。',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tags: ['pages'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							pageId: {
 | 
				
			||||||
 | 
								validator: $.optional.type(ID),
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									'ja-JP': '対象のページのID',
 | 
				
			||||||
 | 
									'en-US': 'Target page ID.'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							name: {
 | 
				
			||||||
 | 
								validator: $.optional.str,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							username: {
 | 
				
			||||||
 | 
								validator: $.optional.str,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res: {
 | 
				
			||||||
 | 
							type: types.object,
 | 
				
			||||||
 | 
							optional: bool.false, nullable: bool.false,
 | 
				
			||||||
 | 
							ref: 'Page',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						errors: {
 | 
				
			||||||
 | 
							noSuchPage: {
 | 
				
			||||||
 | 
								message: 'No such page.',
 | 
				
			||||||
 | 
								code: 'NO_SUCH_PAGE',
 | 
				
			||||||
 | 
								id: '222120c0-3ead-4528-811b-b96f233388d7'
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default define(meta, async (ps, user) => {
 | 
				
			||||||
 | 
						let page: Page | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (ps.pageId) {
 | 
				
			||||||
 | 
							page = await Pages.findOne(ps.pageId);
 | 
				
			||||||
 | 
						} else if (ps.name && ps.username) {
 | 
				
			||||||
 | 
							const author = await Users.findOne({
 | 
				
			||||||
 | 
								host: null,
 | 
				
			||||||
 | 
								usernameLower: ps.username.toLowerCase()
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							if (author) {
 | 
				
			||||||
 | 
								page = await Pages.findOne({
 | 
				
			||||||
 | 
									name: ps.name,
 | 
				
			||||||
 | 
									userId: author.id
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (page == null) {
 | 
				
			||||||
 | 
							throw new ApiError(meta.errors.noSuchPage);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return await Pages.pack(page);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										123
									
								
								src/server/api/endpoints/pages/update.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/server/api/endpoints/pages/update.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,123 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import * as ms from 'ms';
 | 
				
			||||||
 | 
					import define from '../../define';
 | 
				
			||||||
 | 
					import { ApiError } from '../../error';
 | 
				
			||||||
 | 
					import { Pages, DriveFiles } from '../../../../models';
 | 
				
			||||||
 | 
					import { ID } from '../../../../misc/cafy-id';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						desc: {
 | 
				
			||||||
 | 
							'ja-JP': '指定したページの情報を更新します。',
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tags: ['pages'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						kind: 'write:pages',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						limit: {
 | 
				
			||||||
 | 
							duration: ms('1hour'),
 | 
				
			||||||
 | 
							max: 300
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							pageId: {
 | 
				
			||||||
 | 
								validator: $.type(ID),
 | 
				
			||||||
 | 
								desc: {
 | 
				
			||||||
 | 
									'ja-JP': '対象のページのID',
 | 
				
			||||||
 | 
									'en-US': 'Target page ID.'
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							title: {
 | 
				
			||||||
 | 
								validator: $.str,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							name: {
 | 
				
			||||||
 | 
								validator: $.optional.str,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							summary: {
 | 
				
			||||||
 | 
								validator: $.optional.nullable.str,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							content: {
 | 
				
			||||||
 | 
								validator: $.arr($.obj())
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							variables: {
 | 
				
			||||||
 | 
								validator: $.arr($.obj())
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							eyeCatchingImageId: {
 | 
				
			||||||
 | 
								validator: $.optional.nullable.type(ID),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							font: {
 | 
				
			||||||
 | 
								validator: $.optional.str.or(['serif', 'sans-serif']),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							alignCenter: {
 | 
				
			||||||
 | 
								validator: $.optional.bool,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						errors: {
 | 
				
			||||||
 | 
							noSuchPage: {
 | 
				
			||||||
 | 
								message: 'No such page.',
 | 
				
			||||||
 | 
								code: 'NO_SUCH_PAGE',
 | 
				
			||||||
 | 
								id: '21149b9e-3616-4778-9592-c4ce89f5a864'
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							accessDenied: {
 | 
				
			||||||
 | 
								message: 'Access denied.',
 | 
				
			||||||
 | 
								code: 'ACCESS_DENIED',
 | 
				
			||||||
 | 
								id: '3c15cd52-3b4b-4274-967d-6456fc4f792b'
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							noSuchFile: {
 | 
				
			||||||
 | 
								message: 'No such file.',
 | 
				
			||||||
 | 
								code: 'NO_SUCH_FILE',
 | 
				
			||||||
 | 
								id: 'cfc23c7c-3887-490e-af30-0ed576703c82'
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default define(meta, async (ps, user) => {
 | 
				
			||||||
 | 
						const page = await Pages.findOne(ps.pageId);
 | 
				
			||||||
 | 
						if (page == null) {
 | 
				
			||||||
 | 
							throw new ApiError(meta.errors.noSuchPage);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (page.userId !== user.id) {
 | 
				
			||||||
 | 
							throw new ApiError(meta.errors.accessDenied);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let eyeCatchingImage = null;
 | 
				
			||||||
 | 
						if (ps.eyeCatchingImageId != null) {
 | 
				
			||||||
 | 
							eyeCatchingImage = await DriveFiles.findOne({
 | 
				
			||||||
 | 
								id: ps.eyeCatchingImageId,
 | 
				
			||||||
 | 
								userId: user.id
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (eyeCatchingImage == null) {
 | 
				
			||||||
 | 
								throw new ApiError(meta.errors.noSuchFile);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await Pages.update(page.id, {
 | 
				
			||||||
 | 
							updatedAt: new Date(),
 | 
				
			||||||
 | 
							title: ps.title,
 | 
				
			||||||
 | 
							name: ps.name === undefined ? page.name : ps.name,
 | 
				
			||||||
 | 
							summary: ps.name === undefined ? page.summary : ps.summary,
 | 
				
			||||||
 | 
							content: ps.content,
 | 
				
			||||||
 | 
							variables: ps.variables,
 | 
				
			||||||
 | 
							alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
 | 
				
			||||||
 | 
							font: ps.font === undefined ? page.font : ps.font,
 | 
				
			||||||
 | 
							eyeCatchingImageId: ps.eyeCatchingImageId === null
 | 
				
			||||||
 | 
								? null
 | 
				
			||||||
 | 
								: ps.eyeCatchingImageId === undefined
 | 
				
			||||||
 | 
									? page.eyeCatchingImageId
 | 
				
			||||||
 | 
									: eyeCatchingImage!.id,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ import { fetchMeta } from '../../misc/fetch-meta';
 | 
				
			||||||
import * as pkg from '../../../package.json';
 | 
					import * as pkg from '../../../package.json';
 | 
				
			||||||
import { genOpenapiSpec } from '../api/openapi/gen-spec';
 | 
					import { genOpenapiSpec } from '../api/openapi/gen-spec';
 | 
				
			||||||
import config from '../../config';
 | 
					import config from '../../config';
 | 
				
			||||||
import { Users, Notes, Emojis, UserProfiles } from '../../models';
 | 
					import { Users, Notes, Emojis, UserProfiles, Pages } from '../../models';
 | 
				
			||||||
import parseAcct from '../../misc/acct/parse';
 | 
					import parseAcct from '../../misc/acct/parse';
 | 
				
			||||||
import getNoteSummary from '../../misc/get-note-summary';
 | 
					import getNoteSummary from '../../misc/get-note-summary';
 | 
				
			||||||
import { ensure } from '../../prelude/ensure';
 | 
					import { ensure } from '../../prelude/ensure';
 | 
				
			||||||
| 
						 | 
					@ -203,6 +203,41 @@ router.get('/notes/:note', async ctx => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx.status = 404;
 | 
						ctx.status = 404;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Page
 | 
				
			||||||
 | 
					router.get('/@:user/pages/:page', async ctx => {
 | 
				
			||||||
 | 
						const { username, host } = parseAcct(ctx.params.user);
 | 
				
			||||||
 | 
						const user = await Users.findOne({
 | 
				
			||||||
 | 
							usernameLower: username.toLowerCase(),
 | 
				
			||||||
 | 
							host
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (user == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const page = await Pages.findOne({
 | 
				
			||||||
 | 
							name: ctx.params.page,
 | 
				
			||||||
 | 
							userId: user.id
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (page) {
 | 
				
			||||||
 | 
							const _page = await Pages.pack(page);
 | 
				
			||||||
 | 
							const meta = await fetchMeta();
 | 
				
			||||||
 | 
							await ctx.render('page', {
 | 
				
			||||||
 | 
								page: _page,
 | 
				
			||||||
 | 
								instanceName: meta.name || 'Misskey'
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (['public'].includes(page.visibility)) {
 | 
				
			||||||
 | 
								ctx.set('Cache-Control', 'public, max-age=180');
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx.status = 404;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
//#endregion
 | 
					//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.get('/info', async ctx => {
 | 
					router.get('/info', async ctx => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,7 @@ block meta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	meta(name='twitter:card' content='summary')
 | 
						meta(name='twitter:card' content='summary')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// todo
 | 
				
			||||||
	if user.twitter
 | 
						if user.twitter
 | 
				
			||||||
		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
 | 
							meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										30
									
								
								src/server/web/views/page.pug
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/server/web/views/page.pug
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					extends ./base
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					block vars
 | 
				
			||||||
 | 
						- const user = page.user;
 | 
				
			||||||
 | 
						- const title = page.title;
 | 
				
			||||||
 | 
						- const url = `${config.url}/@${user.username}/${page.name}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					block title
 | 
				
			||||||
 | 
						= `${title} | ${instanceName}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					block desc
 | 
				
			||||||
 | 
						meta(name='description' content= page.summary)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					block og
 | 
				
			||||||
 | 
						meta(property='og:type'        content='article')
 | 
				
			||||||
 | 
						meta(property='og:title'       content= title)
 | 
				
			||||||
 | 
						meta(property='og:description' content= page.summary)
 | 
				
			||||||
 | 
						meta(property='og:url'         content= url)
 | 
				
			||||||
 | 
						meta(property='og:image'       content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : user.avatarUrl)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					block meta
 | 
				
			||||||
 | 
						meta(name='misskey:user-username' content=user.username)
 | 
				
			||||||
 | 
						meta(name='misskey:user-id' content=user.id)
 | 
				
			||||||
 | 
						meta(name='misskey:page-id' content=page.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						meta(name='twitter:card' content='summary')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// todo
 | 
				
			||||||
 | 
						if user.twitter
 | 
				
			||||||
 | 
							meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue