Compare commits
58 Commits
Author | SHA1 | Date | |
---|---|---|---|
f8a838f5d7 | |||
ff55062850 | |||
c94dd8b761 | |||
06f8b9da85 | |||
bbe6dbb2ca | |||
3c02691511 | |||
76367bbd25 | |||
8eb28f0115 | |||
79cd1129fd | |||
4c929a14fa | |||
2d3f8a8bd7 | |||
cbcb007517 | |||
f5603ad884 | |||
202b6c1a10 | |||
c1f42ed4f7 | |||
634347a958 | |||
9039dfb34e | |||
0b24b7cc05 | |||
4e4bc3345d | |||
4a2ff8fce6 | |||
3a42c58013 | |||
b6f50bbf53 | |||
21b2f1e555 | |||
7e01edffbe | |||
054e349e6b | |||
9bc387cb86 | |||
49bd6ea363 | |||
9ad11f4297 | |||
8af78a26ba | |||
031c3dee3b | |||
1f3f4a7370 | |||
e36fc53df8 | |||
280a180d9e | |||
509d433959 | |||
169b5c0209 | |||
276c4f5dfe | |||
18d70382ff | |||
09154f1359 | |||
fc1aef6eb7 | |||
5e2c6e6c3b | |||
ff9f9b574b | |||
86b7fd85af | |||
e0995b312c | |||
e9e80bdeb5 | |||
a8119e8366 | |||
43aad8c2d2 | |||
c3bfb2069c | |||
fbf45dab57 | |||
73b1e376a3 | |||
012a02751c | |||
634fedf17c | |||
a5efec89f2 | |||
8bb9816cd0 | |||
05e8782557 | |||
e986ff8c5f | |||
c616214c3b | |||
f552cdcf74 | |||
d187ca0a88 |
2
.gitignore
vendored
@ -29,3 +29,5 @@ coverage
|
|||||||
|
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
*.lockb
|
*.lockb
|
||||||
|
|
||||||
|
*dist
|
41
README.md
@ -1,39 +1,6 @@
|
|||||||
# @hydrogen/solaragent
|
# SolarAgent
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite.
|
Hola! This is Project Hydrogen's universal frontend.
|
||||||
|
Integrated support for Identity, Interactive and Messaging!
|
||||||
|
|
||||||
## Recommended IDE Setup
|
Also provide a mobile version that powered by capacitor!
|
||||||
|
|
||||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
|
||||||
|
|
||||||
## Type Support for `.vue` Imports in TS
|
|
||||||
|
|
||||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
|
||||||
|
|
||||||
## Customize configuration
|
|
||||||
|
|
||||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
|
||||||
|
|
||||||
## Project Setup
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bun dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type-Check, Compile and Minify for Production
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bun build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lint with [ESLint](https://eslint.org/)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bun lint
|
|
||||||
```
|
|
@ -9,6 +9,7 @@ android {
|
|||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':capacitor-local-notifications')
|
||||||
implementation project(':capacitor-preferences')
|
implementation project(':capacitor-preferences')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,46 +1,48 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:label="Solian"
|
||||||
android:label="@string/app_name"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
android:name=".MainActivity"
|
||||||
android:name=".MainActivity"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
android:label="@string/title_activity_main"
|
android:exported="true"
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
android:launchMode="singleTask"
|
||||||
android:launchMode="singleTask"
|
android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||||
android:exported="true">
|
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths"></meta-data>
|
android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
android:maxSdkVersion="32" />
|
<uses-permission
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29" />
|
android:maxSdkVersion="32" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 17 KiB |
@ -1,34 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:viewportWidth="108">
|
|
||||||
<path
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:strokeWidth="1">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="78.5885"
|
|
||||||
android:endY="90.9159"
|
|
||||||
android:startX="48.7653"
|
|
||||||
android:startY="61.0927"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
|
||||||
android:strokeColor="#00000000"
|
|
||||||
android:strokeWidth="1" />
|
|
||||||
</vector>
|
|
@ -5,166 +5,6 @@
|
|||||||
android:viewportHeight="108"
|
android:viewportHeight="108"
|
||||||
android:viewportWidth="108">
|
android:viewportWidth="108">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#26A69A"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeColor="#33FFFFFF"
|
|
||||||
android:strokeWidth="0.8" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
BIN
android/app/src/main/res/drawable/icon_foreground.png
Executable file
After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 3.9 KiB |
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
Before Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 845 B |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 820 B |
Before Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 4.1 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 4.5 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 6.9 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 6.9 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 9.7 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 8.6 KiB |
@ -17,6 +17,5 @@
|
|||||||
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
<item name="android:background">@drawable/splash</item>
|
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
@ -2,5 +2,8 @@
|
|||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':capacitor-local-notifications'
|
||||||
|
project(':capacitor-local-notifications').projectDir = new File('../node_modules/@capacitor/local-notifications/android')
|
||||||
|
|
||||||
include ':capacitor-preferences'
|
include ':capacitor-preferences'
|
||||||
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')
|
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')
|
||||||
|
22
index.html
@ -1,13 +1,15 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/xml+svg" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<link rel="apple-touch-icon" type="image/png" href="/apple-touch-icon.png" sizes="1024x1024">
|
||||||
<title>Solian</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
</head>
|
<script src="https://meet.element.io/external_api.js"></script>
|
||||||
<body>
|
<title>Solian</title>
|
||||||
<div id="app"></div>
|
</head>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<body>
|
||||||
</body>
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||||
|
730477372BB91A4200A78988 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
||||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
@ -73,6 +74,7 @@
|
|||||||
504EC3061FED79650016851F /* App */ = {
|
504EC3061FED79650016851F /* App */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
730477372BB91A4200A78988 /* App.entitlements */,
|
||||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||||
@ -345,6 +347,7 @@
|
|||||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
@ -367,6 +370,7 @@
|
|||||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||||
|
12
ios/App/App/App.entitlements
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
<key>com.apple.developer.associated-domains</key>
|
||||||
|
<array>
|
||||||
|
<string>webcredentials:solsynth.dev</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -20,8 +20,21 @@
|
|||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string></string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Allow Solian use your camera so that you can take photo for your post.</string>
|
||||||
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
|
<string>Allow Solian full access your photo library so that you can share photos more easily.</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>Allow Solian access your photo library so that you can share photos.</string>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@ -43,15 +56,7 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>LSApplicationCategoryType</key>
|
|
||||||
<string></string>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
|
||||||
<string>Allow Solian full access your photo library so that you can share photos more easily.</string>
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
|
||||||
<string>Allow Solian access your photo library so that you can share photos.</string>
|
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>Allow Solian use your camera so that you can take photo for your post.</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications'
|
||||||
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2,12 +2,15 @@ PODS:
|
|||||||
- Capacitor (5.7.4):
|
- Capacitor (5.7.4):
|
||||||
- CapacitorCordova
|
- CapacitorCordova
|
||||||
- CapacitorCordova (5.7.4)
|
- CapacitorCordova (5.7.4)
|
||||||
|
- CapacitorLocalNotifications (5.0.7):
|
||||||
|
- Capacitor
|
||||||
- CapacitorPreferences (5.0.7):
|
- CapacitorPreferences (5.0.7):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||||
|
- "CapacitorLocalNotifications (from `../../node_modules/@capacitor/local-notifications`)"
|
||||||
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
|
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
@ -15,14 +18,17 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../node_modules/@capacitor/ios"
|
:path: "../../node_modules/@capacitor/ios"
|
||||||
CapacitorCordova:
|
CapacitorCordova:
|
||||||
:path: "../../node_modules/@capacitor/ios"
|
:path: "../../node_modules/@capacitor/ios"
|
||||||
|
CapacitorLocalNotifications:
|
||||||
|
:path: "../../node_modules/@capacitor/local-notifications"
|
||||||
CapacitorPreferences:
|
CapacitorPreferences:
|
||||||
:path: "../../node_modules/@capacitor/preferences"
|
:path: "../../node_modules/@capacitor/preferences"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Capacitor: 4fe9adf012caceb4c71ffea2f1f4d005cdcbeea7
|
Capacitor: 4fe9adf012caceb4c71ffea2f1f4d005cdcbeea7
|
||||||
CapacitorCordova: a6e87fccc0307dee7aec1560ec9398485f2b0ce7
|
CapacitorCordova: a6e87fccc0307dee7aec1560ec9398485f2b0ce7
|
||||||
|
CapacitorLocalNotifications: c58afadd159f6bc540ef9b3cbdbc82510a2bf112
|
||||||
CapacitorPreferences: 77ac427e98db83bace772455f8ba447430382c4c
|
CapacitorPreferences: 77ac427e98db83bace772455f8ba447430382c4c
|
||||||
|
|
||||||
PODFILE CHECKSUM: 769e120bf4dfe4ef1095b83775e36bafeeeb3cdd
|
PODFILE CHECKSUM: 19c3106e1cb0c8c0ae26243bfb70b974f8cfaaf5
|
||||||
|
|
||||||
COCOAPODS: 1.15.1
|
COCOAPODS: 1.15.1
|
||||||
|
@ -16,14 +16,19 @@
|
|||||||
"@capacitor/android": "^5.7.4",
|
"@capacitor/android": "^5.7.4",
|
||||||
"@capacitor/core": "^5.7.4",
|
"@capacitor/core": "^5.7.4",
|
||||||
"@capacitor/ios": "^5.7.4",
|
"@capacitor/ios": "^5.7.4",
|
||||||
|
"@capacitor/local-notifications": "^5.0.7",
|
||||||
"@capacitor/preferences": "^5.0.7",
|
"@capacitor/preferences": "^5.0.7",
|
||||||
"@fontsource/roboto": "^5.0.12",
|
"@fontsource/roboto": "^5.0.12",
|
||||||
"@mdi/font": "^7.4.47",
|
"@mdi/font": "^7.4.47",
|
||||||
|
"@vueuse/core": "^10.9.0",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
"dompurify": "^3.0.11",
|
"dompurify": "^3.0.11",
|
||||||
"marked": "^12.0.1",
|
"marked": "^12.0.1",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"universal-cookie": "^7.1.0",
|
"universal-cookie": "^7.1.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
|
"vue-advanced-cropper": "^2.8.8",
|
||||||
"vue-easy-lightbox": "^1.19.0",
|
"vue-easy-lightbox": "^1.19.0",
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.3.0",
|
||||||
"vuetify": "^3.5.12"
|
"vuetify": "^3.5.12"
|
||||||
@ -35,6 +40,8 @@
|
|||||||
"@tsconfig/node20": "^20.1.2",
|
"@tsconfig/node20": "^20.1.2",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/node": "^20.11.28",
|
"@types/node": "^20.11.28",
|
||||||
|
"@types/nprogress": "^0.2.3",
|
||||||
|
"@types/pulltorefreshjs": "^0.1.7",
|
||||||
"@unocss/reset": "^0.58.7",
|
"@unocss/reset": "^0.58.7",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
@ -45,6 +52,7 @@
|
|||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"npm-run-all2": "^6.1.2",
|
"npm-run-all2": "^6.1.2",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
|
"pulltorefreshjs": "^0.1.22",
|
||||||
"typescript": "~5.4.0",
|
"typescript": "~5.4.0",
|
||||||
"unocss": "^0.58.7",
|
"unocss": "^0.58.7",
|
||||||
"vite": "^5.1.6",
|
"vite": "^5.1.6",
|
||||||
|
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
public/icon.png
Normal file
After Width: | Height: | Size: 85 KiB |
@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "../icons/icon-48.webp",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "48x48",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "../icons/icon-72.webp",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "72x72",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "../icons/icon-96.webp",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "../icons/icon-128.webp",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "128x128",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "../icons/icon-192.webp",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "../icons/icon-256.webp",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "256x256",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "../icons/icon-512.webp",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
BIN
public/maskable-icon.png
Normal file
After Width: | Height: | Size: 75 KiB |
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
@ -2,7 +2,6 @@ html,
|
|||||||
body,
|
body,
|
||||||
#app,
|
#app,
|
||||||
.v-application {
|
.v-application {
|
||||||
overflow: auto !important;
|
|
||||||
font-family: "Roboto Sans", ui-sans-serif, system-ui, sans-serif;
|
font-family: "Roboto Sans", ui-sans-serif, system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,4 +11,13 @@ body,
|
|||||||
|
|
||||||
.no-scrollbar::-webkit-scrollbar {
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .bar {
|
||||||
|
background: #ffffff !important;
|
||||||
}
|
}
|
@ -1,89 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-menu eager :close-on-content-click="false">
|
|
||||||
<template #activator="{ props }">
|
|
||||||
<v-btn v-bind="props" stacked rounded="circle" size="small" variant="text" :loading="loading">
|
|
||||||
<v-badge v-if="pagination.total > 0" color="error" :content="pagination.total">
|
|
||||||
<v-icon icon="mdi-bell" />
|
|
||||||
</v-badge>
|
|
||||||
|
|
||||||
<v-icon v-else icon="mdi-bell" />
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<v-list v-if="notifications.length <= 0" class="w-[380px]" density="compact">
|
|
||||||
<v-list-item>
|
|
||||||
<v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for
|
|
||||||
you.</v-alert>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
|
|
||||||
<v-list v-else class="w-[380px]" density="compact" lines="three">
|
|
||||||
<v-list-item v-for="item in notifications">
|
|
||||||
<template #title>{{ item.subject }}</template>
|
|
||||||
<template #subtitle>{{ item.content }}</template>
|
|
||||||
|
|
||||||
<template #append>
|
|
||||||
<v-btn icon="mdi-check" size="x-small" variant="text" :disabled="loading" @click="markAsRead(item)" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="flex text-xs gap-1">
|
|
||||||
<a v-for="link in item.links" class="mt-1 underline" target="_blank" :href="link.url">{{ link.label }}</a>
|
|
||||||
</div>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { request } from "@/scripts/request"
|
|
||||||
import { getAtk } from "@/stores/userinfo"
|
|
||||||
import { reactive, ref } from "vue"
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
|
|
||||||
const notifications = ref<any[]>([])
|
|
||||||
const pagination = reactive({ page: 1, pageSize: 25, total: 0 })
|
|
||||||
|
|
||||||
async function readNotifications() {
|
|
||||||
loading.value = true
|
|
||||||
const res = await request(
|
|
||||||
"identity",
|
|
||||||
"/api/notifications?" +
|
|
||||||
new URLSearchParams({
|
|
||||||
take: pagination.pageSize.toString(),
|
|
||||||
offset: ((pagination.page - 1) * pagination.pageSize).toString()
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
headers: { Authorization: `Bearer ${await getAtk()}` }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (res.status === 200) {
|
|
||||||
const data = await res.json()
|
|
||||||
notifications.value = data["data"]
|
|
||||||
pagination.total = data["count"]
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
readNotifications()
|
|
||||||
|
|
||||||
async function markAsRead(item: any) {
|
|
||||||
loading.value = true
|
|
||||||
const res = await request("identity", `/api/notifications/${item.id}/read`, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: { Authorization: `Bearer ${await getAtk()}` }
|
|
||||||
})
|
|
||||||
if (res.status !== 200) {
|
|
||||||
error.value = await res.text()
|
|
||||||
} else {
|
|
||||||
await readNotifications()
|
|
||||||
error.value = null
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,43 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-menu>
|
|
||||||
<template #activator="{ props }">
|
|
||||||
<v-btn flat exact v-bind="props" icon>
|
|
||||||
<v-avatar color="transparent" icon="mdi-account-circle" :image="'/api/avatar/' + id.userinfo.data?.avatar" />
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<v-list density="compact" v-if="!id.userinfo.isLoggedIn">
|
|
||||||
<v-list-item title="Sign in" prepend-icon="mdi-login-variant" :to="{ name: 'auth.sign-in' }" />
|
|
||||||
<v-list-item title="Create account" prepend-icon="mdi-account-plus" :to="{ name: 'auth.sign-up' }" />
|
|
||||||
</v-list>
|
|
||||||
<v-list density="compact" v-else>
|
|
||||||
<v-list-item :title="nickname" :subtitle="username" />
|
|
||||||
|
|
||||||
<v-divider class="border-opacity-50 my-2" />
|
|
||||||
|
|
||||||
<v-list-item title="User Center" prepend-icon="mdi-account-supervisor" exact :to="{ name: 'dashboard' }" />
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useUserinfo } from "@/stores/userinfo"
|
|
||||||
import { computed } from "vue"
|
|
||||||
|
|
||||||
const id = useUserinfo()
|
|
||||||
|
|
||||||
const username = computed(() => {
|
|
||||||
if (id.userinfo.isLoggedIn) {
|
|
||||||
return "@" + id.userinfo.data?.name
|
|
||||||
} else {
|
|
||||||
return "@vistor"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const nickname = computed(() => {
|
|
||||||
if (id.userinfo.isLoggedIn) {
|
|
||||||
return id.userinfo.data?.nick
|
|
||||||
} else {
|
|
||||||
return "Anonymous"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
209
src/components/chat/ChatEditor.vue
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
<template>
|
||||||
|
<v-form ref="chat" @submit.prevent="sendMessage">
|
||||||
|
<v-expand-transition>
|
||||||
|
<v-alert
|
||||||
|
v-show="channels.related?.messages?.reply_to"
|
||||||
|
class="mb-2 text-sm"
|
||||||
|
variant="elevated"
|
||||||
|
density="compact"
|
||||||
|
type="info"
|
||||||
|
>
|
||||||
|
You are about replying a message #{{ channels.related?.messages?.reply_to?.id }}
|
||||||
|
|
||||||
|
<template #prepend>
|
||||||
|
<div class="h-[30px] flex items-center justify-center">
|
||||||
|
<v-icon icon="mdi-reply" size="small" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
size="x-small"
|
||||||
|
color="white"
|
||||||
|
variant="text"
|
||||||
|
@click="channels.related.messages.reply_to = null"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-alert>
|
||||||
|
</v-expand-transition>
|
||||||
|
|
||||||
|
<v-expand-transition>
|
||||||
|
<v-alert
|
||||||
|
v-show="channels.related?.messages?.edit_to"
|
||||||
|
class="mb-2 text-sm"
|
||||||
|
variant="elevated"
|
||||||
|
density="compact"
|
||||||
|
type="info"
|
||||||
|
>
|
||||||
|
You are about editing a message #{{ channels.related?.messages?.edit_to?.id }}
|
||||||
|
|
||||||
|
<template #prepend>
|
||||||
|
<div class="h-[30px] flex items-center justify-center">
|
||||||
|
<v-icon icon="mdi-pencil" size="small" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
size="x-small"
|
||||||
|
color="white"
|
||||||
|
variant="text"
|
||||||
|
@click="channels.related.messages.edit_to = null"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-alert>
|
||||||
|
</v-expand-transition>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
auto-grow
|
||||||
|
hide-details
|
||||||
|
class="w-full"
|
||||||
|
density="comfortable"
|
||||||
|
placeholder="Enter some messages..."
|
||||||
|
:rows="1"
|
||||||
|
:max-rows="6"
|
||||||
|
:loading="loading"
|
||||||
|
v-model="data.content"
|
||||||
|
@keydown="onEditorKeydown"
|
||||||
|
@paste="pasteMedia"
|
||||||
|
>
|
||||||
|
<template #append-inner>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
type="button"
|
||||||
|
color="teal"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
:disabled="loading"
|
||||||
|
@click.stop="dialogs.attachments = true"
|
||||||
|
>
|
||||||
|
<v-badge v-if="data.attachments.length > 0" :content="data.attachments.length">
|
||||||
|
<v-icon icon="mdi-paperclip" />
|
||||||
|
</v-badge>
|
||||||
|
|
||||||
|
<v-icon v-else icon="mdi-paperclip" />
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn type="submit" icon="mdi-send" size="small" variant="text" @click.stop :disabled="loading" />
|
||||||
|
</template>
|
||||||
|
</v-textarea>
|
||||||
|
|
||||||
|
<Attachments
|
||||||
|
ref="attachments"
|
||||||
|
v-model:show="dialogs.attachments"
|
||||||
|
v-model:uploading="uploading"
|
||||||
|
v-model:value="data.attachments"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-snackbar v-model="uploading" :timeout="-1">
|
||||||
|
Uploading your media, please stand by...
|
||||||
|
<v-progress-linear class="snackbar-progress" indeterminate />
|
||||||
|
</v-snackbar>
|
||||||
|
</v-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, watch } from "vue"
|
||||||
|
import { request } from "@/scripts/request"
|
||||||
|
import { getAtk } from "@/stores/userinfo"
|
||||||
|
import { useChannels } from "@/stores/channels"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
import Attachments from "@/components/chat/parts/ChatAttachments.vue"
|
||||||
|
|
||||||
|
const emits = defineEmits(["sent"])
|
||||||
|
|
||||||
|
const chat = ref<HTMLFormElement>()
|
||||||
|
const channels = useChannels()
|
||||||
|
|
||||||
|
const { showErrorSnackbar } = useUI()
|
||||||
|
const uploading = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const attachments = ref<any>()
|
||||||
|
|
||||||
|
const dialogs = reactive({
|
||||||
|
attachments: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = ref<any>({
|
||||||
|
content: "",
|
||||||
|
reply_id: null,
|
||||||
|
attachments: []
|
||||||
|
})
|
||||||
|
|
||||||
|
async function sendMessage() {
|
||||||
|
if (!data.value.content && !data.value.attachments) return
|
||||||
|
|
||||||
|
const url = channels.related.messages.edit_to
|
||||||
|
? `/api/channels/${channels.current.alias}/messages/${channels.related.messages.edit_to?.id}`
|
||||||
|
: `/api/channels/${channels.current.alias}/messages`
|
||||||
|
const method = channels.related.messages.edit_to ? "PUT" : "POST"
|
||||||
|
|
||||||
|
const payload = data.value
|
||||||
|
payload.reply_to = payload.reply_id
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const res = await request("messaging", url, {
|
||||||
|
method: method,
|
||||||
|
headers: { Authorization: `Bearer ${await getAtk()}`, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
showErrorSnackbar(await res.text())
|
||||||
|
} else {
|
||||||
|
emits("sent")
|
||||||
|
resetEditor()
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditorKeydown(event: KeyboardEvent) {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "enter") {
|
||||||
|
sendMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => channels.related.messages.reply_to,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
data.value.reply_id = val.id
|
||||||
|
} else {
|
||||||
|
data.value.reply_id = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => channels.related.messages.edit_to,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
data.value = val
|
||||||
|
} else {
|
||||||
|
resetEditor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function resetEditor() {
|
||||||
|
chat.value?.reset()
|
||||||
|
channels.related.messages.reply_to = null
|
||||||
|
channels.related.messages.edit_to = null
|
||||||
|
channels.related.messages.delete_to = null
|
||||||
|
data.value = {
|
||||||
|
content: "",
|
||||||
|
attachments: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteMedia(evt: ClipboardEvent) {
|
||||||
|
const files = evt.clipboardData?.files
|
||||||
|
if (files) {
|
||||||
|
Array.from(files).forEach((item) => {
|
||||||
|
attachments.value.upload(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
20
src/components/chat/ChatList.vue
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<v-infinite-scroll
|
||||||
|
class="mt-[-16px] overflow-hidden"
|
||||||
|
:onLoad="props.loader"
|
||||||
|
>
|
||||||
|
<template v-for="item in props.messages" :key="item.id">
|
||||||
|
<chat-message class="px-6 py-2" :id="`m${item.id}`" :item="item" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
<div class="flex-grow-1"></div>
|
||||||
|
</template>
|
||||||
|
</v-infinite-scroll>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ChatMessage from "@/components/chat/ChatMessage.vue"
|
||||||
|
|
||||||
|
const props = defineProps<{ loader: (opts: any) => Promise<any>, messages: any[] }>()
|
||||||
|
</script>
|
125
src/components/chat/ChatMessage.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative transition-colors transition-300 message-item">
|
||||||
|
<a v-if="props.item?.reply_to" :href="`#m${props.item?.reply_to.id}`">
|
||||||
|
<div class="pl-2 mb-0.5 text-sm opacity-80 flex items-center">
|
||||||
|
<v-icon icon="mdi-reply" class="me-2 mb-1" />
|
||||||
|
<v-avatar size="18" class="me-1.5" :image="replyingFromPicture"></v-avatar>
|
||||||
|
<span class="me-1 text-xs overflow-hidden ws-nowarp text-ellipsis">{{ props.item?.reply_to?.content }}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div>
|
||||||
|
<v-avatar
|
||||||
|
color="grey-lighten-2"
|
||||||
|
icon="mdi-account-circle"
|
||||||
|
class="rounded-card"
|
||||||
|
:image="props.item?.sender.account.avatar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="flex gap-1.25 text-sm items-baseline">
|
||||||
|
<span class="font-bold">{{ props.item?.sender.account.nick }}</span>
|
||||||
|
<span class="opacity-80">{{ createdAt }}</span>
|
||||||
|
<span class="opacity-60 text-xs">#{{ props.item?.id }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="props.item?.content"
|
||||||
|
class="prose prose-message max-w-none"
|
||||||
|
v-html="parseContent(props.item?.content ?? '')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<message-attachment
|
||||||
|
v-if="props.item?.attachments && props.item?.attachments.length > 0"
|
||||||
|
class="mt-1"
|
||||||
|
:attachments="props.item?.attachments"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="transition-opacity transition-300 message-action">
|
||||||
|
<v-card>
|
||||||
|
<div class="flex px-2 py-0.5">
|
||||||
|
<v-btn icon="mdi-reply" size="x-small" variant="text" @click="replyMessage" />
|
||||||
|
<v-btn v-if="isOwned" icon="mdi-pencil" size="x-small" variant="text" color="warning"
|
||||||
|
@click="editMessage" />
|
||||||
|
<v-btn v-if="isOwned" icon="mdi-delete" size="x-small" variant="text" color="error"
|
||||||
|
@click="deleteMessage" />
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useChannels } from "@/stores/channels"
|
||||||
|
import { useUserinfo } from "@/stores/userinfo"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { parse } from "marked"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
|
import dompurify from "dompurify"
|
||||||
|
import MessageAttachment from "@/components/chat/renderer/MessageAttachment.vue"
|
||||||
|
|
||||||
|
const id = useUserinfo()
|
||||||
|
const channels = useChannels()
|
||||||
|
|
||||||
|
const props = defineProps<{ item: any }>()
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
const isOwned = computed(() => props.item?.sender?.account_id === id.userinfo.idSet.messaging)
|
||||||
|
const createdAt = computed(() => dayjs(props.item?.created_at).fromNow())
|
||||||
|
|
||||||
|
const replyingFromPicture = computed(() => props.item?.reply_to.sender.account?.avatar ?
|
||||||
|
props.item?.reply_to.sender.account?.avatar :
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
function replyMessage() {
|
||||||
|
channels.related.messages.reply_to = JSON.parse(JSON.stringify(props.item))
|
||||||
|
}
|
||||||
|
|
||||||
|
function editMessage() {
|
||||||
|
channels.related.messages.edit_to = JSON.parse(JSON.stringify(props.item))
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMessage() {
|
||||||
|
channels.related.messages.delete_to = JSON.parse(JSON.stringify(props.item))
|
||||||
|
channels.related.messages.delete_to.channel = channels.current
|
||||||
|
channels.show.messages.delete = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseContent(src: string): string {
|
||||||
|
return dompurify().sanitize(parse(src) as string)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rounded-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-action {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: -18px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, .15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item:hover .message-action {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.prose.prose-message p {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
47
src/components/chat/MessageDeletion.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<v-card title="Delete a message" class="min-h-[540px]" :loading="loading">
|
||||||
|
<template #text>
|
||||||
|
You are deleting a message
|
||||||
|
<b>#{{ channels.related?.messages?.delete_to?.id }}</b> <br />
|
||||||
|
This message will gone and never appear again. But the replies won't affected. Are you confirm?
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<div class="w-full flex justify-end">
|
||||||
|
<v-btn color="grey-darken-3" @click="channels.show.messages.delete = false">Not really</v-btn>
|
||||||
|
<v-btn color="error" :disabled="loading" @click="deleteMessage">Yes</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { request } from "@/scripts/request"
|
||||||
|
import { getAtk } from "@/stores/userinfo"
|
||||||
|
import { useChannels } from "@/stores/channels"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
|
const channels = useChannels()
|
||||||
|
|
||||||
|
const { showSnackbar, showErrorSnackbar } = useUI()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function deleteMessage() {
|
||||||
|
const target = channels.related.messages.delete_to
|
||||||
|
const url = `/api/channels/${target.channel.alias}/messages/${target.id}`
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const res = await request("messaging", url, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { Authorization: `Bearer ${await getAtk()}` }
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
showErrorSnackbar(await res.text())
|
||||||
|
} else {
|
||||||
|
showSnackbar("The message has been deleted.")
|
||||||
|
channels.show.messages.delete = false
|
||||||
|
channels.related.messages.delete_to = null
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
</script>
|
42
src/components/chat/channels/ChannelAction.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<v-menu>
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon="mdi-cog" size="small" variant="text" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list density="compact" lines="one">
|
||||||
|
<v-list-item disabled append-icon="mdi-flag" title="Report" />
|
||||||
|
<v-list-item v-if="isOwned" append-icon="mdi-pencil" title="Edit" @click="editChannel" />
|
||||||
|
<v-list-item v-if="isOwned" append-icon="mdi-delete" title="Delete" @click="deleteChannel" />
|
||||||
|
<v-list-item v-if="isOwned" append-icon="mdi-account-supervisor-circle" title="Members" @click="manageChannel" />
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserinfo } from "@/stores/userinfo"
|
||||||
|
import { useChannels } from "@/stores/channels"
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
|
const id = useUserinfo()
|
||||||
|
const channels = useChannels()
|
||||||
|
|
||||||
|
const props = defineProps<{ item: any }>()
|
||||||
|
|
||||||
|
const isOwned = computed(() => props.item?.account_id === id.userinfo.idSet?.messaging)
|
||||||
|
|
||||||
|
function editChannel() {
|
||||||
|
channels.related.edit_to = props.item
|
||||||
|
channels.show.editor = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function manageChannel() {
|
||||||
|
channels.related.manage_to = props.item
|
||||||
|
channels.show.members = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteChannel() {
|
||||||
|
channels.related.delete_to = props.item
|
||||||
|
channels.show.delete = true
|
||||||
|
}
|
||||||
|
</script>
|
56
src/components/chat/channels/ChannelDeletion.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<v-card title="Delete a realm" class="min-h-[540px]" :loading="loading">
|
||||||
|
<template #text>
|
||||||
|
You are deleting a channel
|
||||||
|
<b>{{ channels.related.delete_to?.name }}</b> <br />
|
||||||
|
All messaging belonging to this channel will be deleted and never appear again. Are you confirm?
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<div class="w-full flex justify-end">
|
||||||
|
<v-btn color="grey-darken-3" @click="channels.show.delete = false">Not really</v-btn>
|
||||||
|
<v-btn color="error" :disabled="loading" @click="deleteChannel">Yes</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { request } from "@/scripts/request"
|
||||||
|
import { getAtk } from "@/stores/userinfo"
|
||||||
|
import { useChannels } from "@/stores/channels"
|
||||||
|
import { useRoute, useRouter } from "vue-router"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const channels = useChannels()
|
||||||
|
|
||||||
|
const emits = defineEmits(["relist"])
|
||||||
|
|
||||||
|
const { showSnackbar, showErrorSnackbar } = useUI()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
async function deleteChannel() {
|
||||||
|
const target = channels.related.delete_to
|
||||||
|
const url = `/api/channels/${target.id}`
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const res = await request("messaging", url, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { Authorization: `Bearer ${await getAtk()}` }
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
showErrorSnackbar(await res.text())
|
||||||
|
} else {
|
||||||
|
showSnackbar("The channel has been deleted.")
|
||||||
|
channels.show.delete = false
|
||||||
|
channels.related.delete_to = null
|
||||||
|
emits("relist")
|
||||||
|
if (route.name?.toString()?.includes("channel")) {
|
||||||
|
await router.push({ name: "explore" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
</script>
|
75
src/components/chat/channels/ChannelEditor.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<v-card title="Establish a channel" prepend-icon="mdi-pound-box" class="min-h-[540px]" :loading="loading">
|
||||||
|
<v-form @submit.prevent="submit">
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field label="Alias" variant="outlined" density="comfortable" hint="Must be unique"
|
||||||
|
v-model="data.alias" />
|
||||||
|
<v-text-field label="Name" variant="outlined" density="comfortable" v-model="data.name" />
|
||||||
|
<v-textarea label="Description" variant="outlined" density="comfortable" v-model="data.description" />
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn type="reset" color="grey-darken-3" @click="channels.show.editor = false">Cancel</v-btn>
|
||||||
|
<v-btn type="submit" :disabled="loading">Save</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from "vue"
|
||||||
|
import { getAtk } from "@/stores/userinfo"
|
||||||
|
import { request } from "@/scripts/request"
|
||||||
|
import { useChannels } from "@/stores/channels"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
|
const emits = defineEmits(["relist"])
|
||||||
|
|
||||||
|
const channels = useChannels()
|
||||||
|
|
||||||
|
const {showErrorSnackbar} = useUI()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const data = ref({
|
||||||
|
alias: "",
|
||||||
|
name: "",
|
||||||
|
description: ""
|
||||||
|
})
|
||||||
|
|
||||||
|
async function submit(evt: SubmitEvent) {
|
||||||
|
const form = evt.target as HTMLFormElement
|
||||||
|
const payload = data.value
|
||||||
|
if (!payload.name) return
|
||||||
|
|
||||||
|
const url = channels.related.edit_to ? `/api/channels/${channels.related.edit_to?.id}` : "/api/channels"
|
||||||
|
const method = channels.related.edit_to ? "PUT" : "POST"
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const res = await request("messaging", url, {
|
||||||
|
method: method,
|
||||||
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAtk()}` },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
showErrorSnackbar(await res.text())
|
||||||
|
} else {
|
||||||
|
emits("relist")
|
||||||
|
form.reset()
|
||||||
|
channels.done = true
|
||||||
|
channels.show.editor = false
|
||||||
|
channels.related.edit_to = null
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
channels.related,
|
||||||
|
(val) => {
|
||||||
|
if (val.edit_to) {
|
||||||
|
data.value = JSON.parse(JSON.stringify(val.edit_to))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
68
src/components/chat/channels/ChannelInvitation.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<v-card prepend-icon="mdi-account-plus" title="Invite someone">
|
||||||
|
<v-form @submit.prevent="inviteMember">
|
||||||
|
<v-card-text>
|
||||||
|
<v-autocomplete
|
||||||
|
label="Username"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
autocomplete="off"
|
||||||
|
hide-selected
|
||||||
|
:items="friends.available.map(x => getOtherside(x).name)"
|
||||||
|
v-model="targetName"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn type="reset" color="grey-darken-3" @click="emits('close')">Cancel</v-btn>
|
||||||
|
<v-btn type="submit" :disabled="loading">Invite</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { request } from "@/scripts/request"
|
||||||
|
import { getAtk, useUserinfo } from "@/stores/userinfo"
|
||||||
|
import { useFriends } from "@/stores/friends"
|
||||||
|
|
||||||
|
const props = defineProps<{ item: any }>()
|
||||||
|
const emits = defineEmits(["close", "error", "relist"])
|
||||||
|
|
||||||
|
const id = useUserinfo()
|
||||||
|
const friends = useFriends()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const targetName = ref("")
|
||||||
|
|
||||||
|
async function inviteMember(evt: SubmitEvent) {
|
||||||
|
const form = evt.target as HTMLFormElement
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const res = await request("messaging", `/api/channels/${props.item?.id}/invite`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAtk()}` },
|
||||||
|
body: JSON.stringify({
|
||||||
|
account_name: targetName.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
emits("error", await res.text())
|
||||||
|
} else {
|
||||||
|
form.reset()
|
||||||
|
emits("relist")
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOtherside(item: any) {
|
||||||
|
if (item.account_id === id.userinfo.data?.id) {
|
||||||
|
return item.related
|
||||||
|
} else {
|
||||||
|
return item.account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
42
src/components/chat/channels/ChannelList.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<v-list-group class="channels-list" value="channels">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
prepend-icon="mdi-chat"
|
||||||
|
title="Channels"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
v-for="item in channels.available"
|
||||||
|
exact
|
||||||
|
append-icon="mdi-pound-box"
|
||||||
|
:to="{ name: 'chat.channel', params: { channel: item.alias } }"
|
||||||
|
:title="item.name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
append-icon="mdi-plus"
|
||||||
|
title="Create a channel"
|
||||||
|
variant="plain"
|
||||||
|
:disabled="!id.userinfo.isLoggedIn"
|
||||||
|
@click="createChannel"
|
||||||
|
/>
|
||||||
|
</v-list-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserinfo } from "@/stores/userinfo"
|
||||||
|
import { useChannels } from "@/stores/channels"
|
||||||
|
|
||||||
|
const id = useUserinfo()
|
||||||
|
const channels = useChannels()
|
||||||
|
|
||||||
|
function createChannel() {
|
||||||
|
channels.related.edit_to = null
|
||||||
|
channels.related.manage_to = null
|
||||||
|
channels.related.delete_to = null
|
||||||
|
channels.show.editor = true
|
||||||
|
}
|
||||||
|
</script>
|
121
src/components/chat/channels/ChannelMembers.vue
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<v-card title="Channel members" class="min-h-[540px]">
|
||||||
|
<v-list density="comfortable" lines="one">
|
||||||
|
<v-list-item v-for="item in members" :title="item.account.nick">
|
||||||
|
<template #subtitle>@{{ item.account.name }}</template>
|
||||||
|
<template #prepend>
|
||||||
|
<v-avatar
|
||||||
|
color="grey-lighten-2"
|
||||||
|
icon="mdi-account-circle"
|
||||||
|
class="rounded-card me-2"
|
||||||
|
size="small"
|
||||||
|
:image="item?.account.avatar"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-account-remove"
|
||||||
|
size="x-small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
:disabled="!checkKickable(item)"
|
||||||
|
@click="kickMember(item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<div v-if="isOwned">
|
||||||
|
<v-divider class="mt-2 mb-3 border-opacity-50 mx-[-1rem]" />
|
||||||
|
|
||||||
|
<div class="px-3">
|
||||||
|
<v-dialog class="max-w-[540px]">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn v-bind="props" block prepend-icon="mdi-account-plus" variant="plain"> Invite someone</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default="{ isActive }">
|
||||||
|
<channel-invitation
|
||||||
|
:item="props.item"
|
||||||
|
@relist="listMembers"
|
||||||
|
@error="(val) => (showErrorSnackbar(val))"
|
||||||
|
@close="isActive.value = false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from "vue"
|
||||||
|
import { request } from "@/scripts/request"
|
||||||
|
import { getAtk, useUserinfo } from "@/stores/userinfo"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import ChannelInvitation from "@/components/chat/channels/ChannelInvitation.vue"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
|
const id = useUserinfo()
|
||||||
|
|
||||||
|
const props = defineProps<{ item: any }>()
|
||||||
|
|
||||||
|
const members = ref<any[]>([])
|
||||||
|
|
||||||
|
const isOwned = computed(() => {
|
||||||
|
return id.userinfo.idSet?.messaging === props.item?.account_id
|
||||||
|
})
|
||||||
|
|
||||||
|
const { showErrorSnackbar } = useUI()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.item,
|
||||||
|
(val) => {
|
||||||
|
if (val?.id) {
|
||||||
|
listMembers(val.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
async function listMembers(id: number) {
|
||||||
|
loading.value = true
|
||||||
|
const res = await request("messaging", `/api/channels/${id}/members`)
|
||||||
|
if (res.status !== 200) {
|
||||||
|
showErrorSnackbar(await res.text())
|
||||||
|
} else {
|
||||||
|
members.value = await res.json()
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function kickMember(item: any) {
|
||||||
|
loading.value = true
|
||||||
|
const res = await request("messaging", `/api/channels/${props.item?.id}/kick`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAtk()}` },
|
||||||
|
body: JSON.stringify({
|
||||||
|
account_name: item.account.name
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
showErrorSnackbar(await res.text())
|
||||||
|
} else {
|
||||||
|
await listMembers(props.item?.id)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkKickable(item: any) {
|
||||||
|
if (item.account?.id === id.userinfo.idSet?.messaging) return false
|
||||||
|
if (item.account?.id === props.item?.account_id) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.rounded-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
25
src/components/chat/channels/ChannelTools.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<v-bottom-sheet class="max-w-[480px]" v-model="channels.show.editor">
|
||||||
|
<channel-editor @relist="channels.list" />
|
||||||
|
</v-bottom-sheet>
|
||||||
|
<v-bottom-sheet class="max-w-[480px]" v-model="channels.show.members">
|
||||||
|
<channel-members :item="channels.related.manage_to" @relist="channels.list" />
|
||||||
|
</v-bottom-sheet>
|
||||||
|
<v-bottom-sheet class="max-w-[480px]" v-model="channels.show.delete">
|
||||||
|
<channel-deletion @relist="channels.list" />
|
||||||
|
</v-bottom-sheet>
|
||||||
|
|
||||||
|
<v-bottom-sheet class="max-w-[480px]" v-model="channels.show.messages.delete">
|
||||||
|
<message-deletion />
|
||||||
|
</v-bottom-sheet>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useChannels } from "@/stores/channels"
|
||||||
|
import ChannelEditor from "@/components/chat/channels/ChannelEditor.vue"
|
||||||
|
import ChannelMembers from "@/components/chat/channels/ChannelMembers.vue"
|
||||||
|
import ChannelDeletion from "@/components/chat/channels/ChannelDeletion.vue"
|
||||||
|
import MessageDeletion from "@/components/chat/MessageDeletion.vue"
|
||||||
|
|
||||||
|
const channels = useChannels()
|
||||||
|
</script>
|
141
src/components/chat/parts/ChatAttachments.vue
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
eager
|
||||||
|
class="max-w-[540px]"
|
||||||
|
:model-value="props.show"
|
||||||
|
@update:model-value="(val) => emits('update:show', val)"
|
||||||
|
>
|
||||||
|
<v-card title="Attachments">
|
||||||
|
<template #text>
|
||||||
|
<v-file-input
|
||||||
|
prepend-icon=""
|
||||||
|
append-icon="mdi-upload"
|
||||||
|
variant="solo-filled"
|
||||||
|
label="File Picker"
|
||||||
|
v-model="picked"
|
||||||
|
:loading="props.uploading"
|
||||||
|
@click:append="upload()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h2 class="px-2 mb-1">Media list</h2>
|
||||||
|
<v-card variant="tonal">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item v-for="(item, idx) in props.value" :title="getFileName(item)">
|
||||||
|
<template #subtitle> {{ getFileType(item) }} · {{ formatBytes(item.filesize) }}</template>
|
||||||
|
<template #append>
|
||||||
|
<v-btn icon="mdi-delete" size="small" variant="text" color="error" @click="dispose(idx)" />
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<v-btn class="ms-auto" text="Ok" @click="emits('update:show', false)"></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { request } from "@/scripts/request"
|
||||||
|
import { getAtk } from "@/stores/userinfo"
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps<{ show: boolean; uploading: boolean; value: any[] }>()
|
||||||
|
const emits = defineEmits(["update:show", "update:uploading", "update:value"])
|
||||||
|
|
||||||
|
const picked = ref<any[]>([])
|
||||||
|
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
async function upload(file?: any) {
|
||||||
|
if (props.uploading) return
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
if (!file) {
|
||||||
|
file = picked.value[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
data.set("attachment", file)
|
||||||
|
data.set("hashcode", await calculateHashCode(file))
|
||||||
|
|
||||||
|
emits("update:uploading", true)
|
||||||
|
const res = await request("messaging", "/api/attachments", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${await getAtk()}` },
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
let meta: any
|
||||||
|
if (res.status !== 200) {
|
||||||
|
error.value = await res.text()
|
||||||
|
} else {
|
||||||
|
meta = await res.json()
|
||||||
|
emits("update:value", props.value.concat([meta.info]))
|
||||||
|
picked.value = []
|
||||||
|
}
|
||||||
|
emits("update:uploading", false)
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dispose(idx: number) {
|
||||||
|
const media = JSON.parse(JSON.stringify(props.value))
|
||||||
|
const item = media.splice(idx)[0]
|
||||||
|
emits("update:value", media)
|
||||||
|
|
||||||
|
const res = await request("messaging", `/api/attachments/${item.id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { Authorization: `Bearer ${await getAtk()}` }
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
error.value = await res.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ upload, dispose })
|
||||||
|
|
||||||
|
async function calculateHashCode(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = async () => {
|
||||||
|
const buffer = reader.result as ArrayBuffer
|
||||||
|
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer)
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
|
const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("")
|
||||||
|
resolve(hashHex)
|
||||||
|
}
|
||||||
|
reader.onerror = () => {
|
||||||
|
reject(reader.error)
|
||||||
|
}
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileName(item: any) {
|
||||||
|
return item.filename.replace(/\.[^/.]+$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileType(item: any) {
|
||||||
|
switch (item.type) {
|
||||||
|
case 1:
|
||||||
|
return "Photo"
|
||||||
|
case 2:
|
||||||
|
return "Video"
|
||||||
|
case 3:
|
||||||
|
return "Audio"
|
||||||
|
default:
|
||||||
|
return "Others"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes: number, decimals = 2) {
|
||||||
|
if (!+bytes) return "0 Bytes"
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const dm = decimals < 0 ? 0 : decimals
|
||||||
|
const sizes = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
||||||
|
}
|
||||||
|
</script>
|
102
src/components/chat/renderer/MessageAttachment.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<v-chip size="small" variant="tonal" prepend-icon="mdi-paperclip" v-if="props.overview">
|
||||||
|
Attached {{ props.attachments.length }} attachment(s)
|
||||||
|
</v-chip>
|
||||||
|
|
||||||
|
<v-card variant="outlined" class="w-fit max-h-[540px]">
|
||||||
|
<v-carousel
|
||||||
|
hide-delimiter-background
|
||||||
|
height="100%"
|
||||||
|
:hide-delimiters="props.attachments.length <= 1"
|
||||||
|
:show-arrows="false"
|
||||||
|
>
|
||||||
|
<v-carousel-item v-for="(item, idx) in attachments">
|
||||||
|
<img
|
||||||
|
v-if="item.type === 1"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
class="cursor-zoom-in content-visibility-auto max-h-[540px] object-cover object-c"
|
||||||
|
:src="getUrl(item)"
|
||||||
|
:alt="item.filename"
|
||||||
|
@click="openLightbox(item, idx)"
|
||||||
|
/>
|
||||||
|
<video v-else-if="item.type === 2" controls class="w-full content-visibility-auto">
|
||||||
|
<source :src="getUrl(item)" />
|
||||||
|
</video>
|
||||||
|
<div v-else-if="item.type === 3" class="py-5 px-2">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="mb-1">{{ getFileName(item) }}</p>
|
||||||
|
<audio controls :src="getUrl(item)" class="mx-auto max-w-[85%]"></audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="py-5 px-2">
|
||||||
|
<div class="text-center">
|
||||||
|
<p>{{ getFileName(item) }}</p>
|
||||||
|
<a class="underline" target="_blank" :href="getUrl(item)">Download</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-carousel-item>
|
||||||
|
</v-carousel>
|
||||||
|
|
||||||
|
<vue-easy-lightbox
|
||||||
|
teleport="#app"
|
||||||
|
:visible="lightbox"
|
||||||
|
:imgs="props.attachments.map((x) => getUrl(x))"
|
||||||
|
v-model:index="currentIndex"
|
||||||
|
@hide="lightbox = false"
|
||||||
|
>
|
||||||
|
<template v-slot:close-btn="{ close }">
|
||||||
|
<v-btn
|
||||||
|
class="fixed left-2 top-2"
|
||||||
|
icon="mdi-close"
|
||||||
|
variant="text"
|
||||||
|
color="white"
|
||||||
|
:style="`margin-top: ${safeAreaTop}`"
|
||||||
|
@click="close"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</vue-easy-lightbox>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { buildRequestUrl } from "@/scripts/request"
|
||||||
|
import { computed, ref } from "vue"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
import VueEasyLightbox from "vue-easy-lightbox"
|
||||||
|
|
||||||
|
const props = defineProps<{ attachments: any[]; overview?: boolean }>()
|
||||||
|
|
||||||
|
const ui = useUI()
|
||||||
|
|
||||||
|
const lightbox = ref(false)
|
||||||
|
|
||||||
|
const current = ref<any>(null)
|
||||||
|
const currentIndex = ref(0)
|
||||||
|
|
||||||
|
const safeAreaTop = computed(() => {
|
||||||
|
return `${ui.safeArea.top}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
function getFileName(item: any) {
|
||||||
|
return item.filename.replace(/\.[^/.]+$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUrl(item: any) {
|
||||||
|
return item.external_url
|
||||||
|
? item.external_url
|
||||||
|
: buildRequestUrl("messaging", `/api/attachments/o/${item.file_id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLightbox(item: any, idx: number) {
|
||||||
|
current.value = item
|
||||||
|
currentIndex.value = idx
|
||||||
|
lightbox.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vel-model {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-xs text-center opacity-80">
|
<div class="text-xs text-center opacity-80">
|
||||||
<p>Copyright © {{ new Date().getFullYear() }} Solsynth</p>
|
<p>Copyright © {{ new Date().getFullYear() }} Solsynth</p>
|
||||||
<p>Powered by <a class="underline" href="#">Hydrogen.Identity</a></p>
|
<p>Powered by <a class="underline" href="#">Hydrogen</a></p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
28
src/components/common/SnackbarProvider.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<v-snackbar v-model="ui.snackbar" v-bind="ui.snackbar">
|
||||||
|
<div v-html="ui.snackbar.content"></div>
|
||||||
|
<v-progress-linear v-if="ui.snackbar.loading" class="snackbar-progress" indeterminate />
|
||||||
|
</v-snackbar>
|
||||||
|
|
||||||
|
<v-snackbar v-model="ui.reconnection.messages">
|
||||||
|
<div>Reconnecting with messaging server...</div>
|
||||||
|
<v-progress-linear v-if="ui.snackbar.loading" class="snackbar-progress" indeterminate />
|
||||||
|
</v-snackbar>
|
||||||
|
<v-snackbar v-model="ui.reconnection.notifications">
|
||||||
|
<div>Reconnecting with notifications server...</div>
|
||||||
|
<v-progress-linear v-if="ui.snackbar.loading" class="snackbar-progress" indeterminate />
|
||||||
|
</v-snackbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
|
const ui = useUI()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.snackbar-progress {
|
||||||
|
margin: 12px -16px -14px;
|
||||||
|
width: calc(100% + 64px);
|
||||||
|
}
|
||||||
|
</style>
|
63
src/components/friends/FriendListItem.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<v-list-item :title="otherside.nick">
|
||||||
|
<template #subtitle>@{{ otherside.name }}</template>
|
||||||
|
<template #prepend>
|
||||||
|
<v-avatar
|
||||||
|
color="grey-lighten-2"
|
||||||
|
icon="mdi-account-circle"
|
||||||
|
class="rounded-card me-2"
|
||||||
|
size="small"
|
||||||
|
:image="othersidePicture"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-check"
|
||||||
|
size="x-small"
|
||||||
|
color="success"
|
||||||
|
variant="text"
|
||||||
|
:disabled="!canApprove"
|
||||||
|
@click="emits('approve')"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
size="x-small"
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
:disabled="!canDecline"
|
||||||
|
@click="emits('decline')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useUserinfo } from "@/stores/userinfo"
|
||||||
|
import { buildRequestUrl } from "@/scripts/request"
|
||||||
|
|
||||||
|
const id = useUserinfo()
|
||||||
|
|
||||||
|
const props = defineProps<{ item: any }>()
|
||||||
|
const emits = defineEmits(["approve", "decline"])
|
||||||
|
|
||||||
|
const canApprove = computed(() => {
|
||||||
|
return props.item.status === 2 ||
|
||||||
|
(props.item.status === 0 && props.item.related_id === id.userinfo.data?.id)
|
||||||
|
})
|
||||||
|
const canDecline = computed(() => {
|
||||||
|
return props.item.status !== 2
|
||||||
|
})
|
||||||
|
|
||||||
|
const otherside = computed(() => {
|
||||||
|
if (props.item.account_id === id.userinfo.data?.id) {
|
||||||
|
return props.item.related
|
||||||
|
} else {
|
||||||
|
return props.item.account
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const othersidePicture = computed(() => otherside.value?.avatar ?
|
||||||
|
buildRequestUrl("identity", `/api/avatar/${otherside.value?.avatar}`) :
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
</script>
|
150
src/components/navigation/NavigationDrawer.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<v-navigation-drawer
|
||||||
|
v-model="ui.drawer.open"
|
||||||
|
floating
|
||||||
|
color="grey-lighten-5"
|
||||||
|
width="320"
|
||||||
|
:permanent="isLargeScreen"
|
||||||
|
:rail="ui.drawer.mini"
|
||||||
|
:rail-width="58"
|
||||||
|
:order="0"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<v-toolbar
|
||||||
|
class="flex items-center justify-between px-[14px] border-opacity-15"
|
||||||
|
color="primary"
|
||||||
|
height="64"
|
||||||
|
:style="`padding-top: ${safeAreaTop}`"
|
||||||
|
@click="expandDrawer"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<img src="/favicon.png" alt="Logo" width="32" height="32" class="block" />
|
||||||
|
<div v-show="!ui.drawer.mini" class="ms-6 font-medium">Solar Network</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<v-btn
|
||||||
|
v-if="isLargeScreen"
|
||||||
|
v-show="!ui.drawer.mini"
|
||||||
|
icon="mdi-arrow-collapse-left"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click.stop="ui.drawer.mini = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<v-list
|
||||||
|
class="nav-list"
|
||||||
|
density="compact"
|
||||||
|
:opened="ui.drawer.mini ? [] : expanded.nav"
|
||||||
|
@update:opened="(val) => expanded.nav = val"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
exact
|
||||||
|
title="Explore"
|
||||||
|
prepend-icon="mdi-compass"
|
||||||
|
:to="{ name: 'explore' }"
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-divider class="border-opacity-75 my-2" />
|
||||||
|
|
||||||
|
<v-list
|
||||||
|
class="resources-list"
|
||||||
|
density="compact"
|
||||||
|
:opened="ui.drawer.mini ? [] : expanded.resources"
|
||||||
|
@update:opened="(val) => expanded.resources = val"
|
||||||
|
@click="expandDrawer"
|
||||||
|
>
|
||||||
|
<channel-list />
|
||||||
|
<realm-list />
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User info -->
|
||||||
|
<v-list
|
||||||
|
class="bg-grey-lighten-3"
|
||||||
|
:style="`margin-bottom: ${safeAreaBottom}`"
|
||||||
|
@click="expandDrawer"
|
||||||
|
>
|
||||||
|
<v-list-item :subtitle="username" :title="nickname">
|
||||||
|
<template #prepend>
|
||||||
|
<v-avatar icon="mdi-account-circle" :image="id.userinfo.data?.picture" />
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<notification-list v-if="id.userinfo.isLoggedIn" />
|
||||||
|
<user-menu />
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive } from "vue"
|
||||||
|
import { useUserinfo } from "@/stores/userinfo"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
import { useRealms } from "@/stores/realms"
|
||||||
|
import { useChannels } from "@/stores/channels"
|
||||||
|
import { useMediaQuery } from "@vueuse/core"
|
||||||
|
import PullToRefresh from "pulltorefreshjs"
|
||||||
|
import UserMenu from "@/components/users/UserMenu.vue"
|
||||||
|
import RealmList from "@/components/realms/RealmList.vue"
|
||||||
|
import ChannelList from "@/components/chat/channels/ChannelList.vue"
|
||||||
|
import NotificationList from "@/components/users/NotificationList.vue"
|
||||||
|
|
||||||
|
const ui = useUI()
|
||||||
|
const expanded = reactive<{ [id: string]: string[] }>({
|
||||||
|
nav: [],
|
||||||
|
resources: ["channels", "realms"]
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLargeScreen = useMediaQuery("(min-width: 768px)")
|
||||||
|
|
||||||
|
const safeAreaTop = computed(() => {
|
||||||
|
return `${ui.safeArea.top}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
const safeAreaBottom = computed(() => {
|
||||||
|
return `${ui.safeArea.bottom}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
const id = useUserinfo()
|
||||||
|
|
||||||
|
const username = computed(() => {
|
||||||
|
if (id.userinfo.isLoggedIn) {
|
||||||
|
return "@" + id.userinfo.data?.name
|
||||||
|
} else {
|
||||||
|
return "@vistor"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const nickname = computed(() => {
|
||||||
|
if (id.userinfo.isLoggedIn) {
|
||||||
|
return id.userinfo.data?.nick
|
||||||
|
} else {
|
||||||
|
return "Anonymous"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function expandDrawer() {
|
||||||
|
ui.drawer.mini = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
PullToRefresh.init({
|
||||||
|
mainElement: ".resources-list",
|
||||||
|
triggerElement: ".resources-list",
|
||||||
|
onRefresh() {
|
||||||
|
return Promise.all([
|
||||||
|
useRealms().list(),
|
||||||
|
useChannels().list()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
@ -22,7 +22,7 @@ const editor = useEditor()
|
|||||||
|
|
||||||
const props = defineProps<{ item: any }>()
|
const props = defineProps<{ item: any }>()
|
||||||
|
|
||||||
const isOwned = computed(() => props.item?.author_id === id.userinfo.data.id)
|
const isOwned = computed(() => props.item?.author_id === id.userinfo.idSet.interactive)
|
||||||
|
|
||||||
function editPost() {
|
function editPost() {
|
||||||
editor.related.edit_to = JSON.parse(JSON.stringify(props.item))
|
editor.related.edit_to = JSON.parse(JSON.stringify(props.item))
|
||||||
|
@ -3,21 +3,47 @@
|
|||||||
Attached {{ props.attachments.length }} attachment(s)
|
Attached {{ props.attachments.length }} attachment(s)
|
||||||
</v-chip>
|
</v-chip>
|
||||||
|
|
||||||
<v-card v-else variant="outlined" class="max-w-[540px] max-h-[720px]">
|
<v-card v-else variant="outlined" class="w-fit max-h-[540px]">
|
||||||
<v-carousel hide-delimiter-background height="100%" :show-arrows="false">
|
<v-carousel
|
||||||
<v-carousel-item v-for="item in attachments">
|
hide-delimiter-background
|
||||||
<img v-if="item.type === 1" :src="getUrl(item)" :alt="item.filename" class="cursor-zoom-in"
|
height="100%"
|
||||||
@click="openLightbox" />
|
:hide-delimiters="props.attachments.length <= 1"
|
||||||
<video v-if="item.type === 2" controls class="w-full">
|
:show-arrows="false"
|
||||||
|
>
|
||||||
|
<v-carousel-item v-for="(item, idx) in attachments">
|
||||||
|
<img
|
||||||
|
v-if="item.type === 1"
|
||||||
|
decoding="async"
|
||||||
|
class="cursor-zoom-in max-h-[540px] object-cover object-c"
|
||||||
|
:src="getUrl(item)"
|
||||||
|
:alt="getFileName(item)"
|
||||||
|
@click="openLightbox(item, idx)"
|
||||||
|
/>
|
||||||
|
<video v-else-if="item.type === 2" controls class="w-full content-visibility-auto">
|
||||||
<source :src="getUrl(item)" />
|
<source :src="getUrl(item)" />
|
||||||
</video>
|
</video>
|
||||||
<div v-if="item.type === 3" class="w-full px-7 py-12">
|
<div v-else-if="item.type === 3" class="py-5 px-2">
|
||||||
<audio controls :src="getUrl(item)" class="mx-auto"></audio>
|
<div class="text-center">
|
||||||
|
<p class="mb-1">{{ getFileName(item) }}</p>
|
||||||
|
<audio controls :src="getUrl(item)" class="mx-auto max-w-[85%]"></audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="py-5 px-2">
|
||||||
|
<div class="text-center">
|
||||||
|
<p>{{ getFileName(item) }}</p>
|
||||||
|
<a class="underline" target="_blank" :href="getUrl(item)">Download</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-carousel-item>
|
</v-carousel-item>
|
||||||
</v-carousel>
|
</v-carousel>
|
||||||
|
|
||||||
<vue-easy-lightbox teleport="#app" :visible="lightbox" :imgs="[getUrl(current)]" @hide="lightbox = false">
|
<vue-easy-lightbox
|
||||||
|
teleport="#app"
|
||||||
|
:visible="lightbox"
|
||||||
|
:imgs="props.attachments.map((x) => getUrl(x))"
|
||||||
|
v-model:index="currentIndex"
|
||||||
|
@hide="lightbox = false"
|
||||||
|
>
|
||||||
<template v-slot:close-btn="{ close }">
|
<template v-slot:close-btn="{ close }">
|
||||||
<v-btn
|
<v-btn
|
||||||
class="fixed left-2 top-2"
|
class="fixed left-2 top-2"
|
||||||
@ -43,23 +69,28 @@ const props = defineProps<{ attachments: any[]; overview?: boolean }>()
|
|||||||
const ui = useUI()
|
const ui = useUI()
|
||||||
|
|
||||||
const lightbox = ref(false)
|
const lightbox = ref(false)
|
||||||
const focus = ref(0)
|
|
||||||
|
|
||||||
const current = computed(() => props.attachments[focus.value])
|
const current = ref<any>(null)
|
||||||
const canLightbox = computed(() => current.value.type === 1)
|
const currentIndex = ref(0)
|
||||||
|
|
||||||
const safeAreaTop = computed(() => {
|
const safeAreaTop = computed(() => {
|
||||||
return `${ui.safeArea.top}px`
|
return `${ui.safeArea.top}px`
|
||||||
})
|
})
|
||||||
|
|
||||||
function getUrl(item: any) {
|
function getFileName(item: any) {
|
||||||
return item.external_url ? item.external_url : buildRequestUrl("interactive", `/api/attachments/o/${item.file_id}`)
|
return item.filename.replace(/\.[^/.]+$/, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
function openLightbox() {
|
function getUrl(item: any) {
|
||||||
if (canLightbox.value) {
|
return item.external_url
|
||||||
lightbox.value = true
|
? item.external_url
|
||||||
}
|
: buildRequestUrl("interactive", `/api/attachments/o/${item.file_id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLightbox(item: any, idx: number) {
|
||||||
|
current.value = item
|
||||||
|
currentIndex.value = idx
|
||||||
|
lightbox.value = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<div>
|
<div>
|
||||||
<v-avatar
|
<router-link :to="{ name: 'users.page', params: { alias: props.item?.author.name ?? 'ghost' } }">
|
||||||
color="grey-lighten-2"
|
<v-avatar
|
||||||
icon="mdi-account-circle"
|
color="grey-lighten-2"
|
||||||
class="rounded-card"
|
icon="mdi-account-circle"
|
||||||
:image="props.item?.author.avatar"
|
class="rounded-card"
|
||||||
/>
|
:image="props.item?.author.avatar"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="post-list">
|
<div class="post-list mx-[-8px]">
|
||||||
<v-infinite-scroll :items="props.posts" :onLoad="props.loader">
|
<v-infinite-scroll :items="props.posts" :onLoad="props.loader">
|
||||||
<template v-for="(item, idx) in props.posts" :key="item">
|
<template v-for="(item, idx) in props.posts" :key="item.id">
|
||||||
<div class="mb-3 px-1">
|
<div class="mb-3 px-[8px]">
|
||||||
<v-card>
|
<v-card :variant="props.variant ?? 'elevated'">
|
||||||
<template #text>
|
<template #text>
|
||||||
<post-item brief :item="item" @update:item="(val) => updateItem(idx, val)" />
|
<post-item brief :item="item" @update:item="(val) => updateItem(idx, val)" />
|
||||||
</template>
|
</template>
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PostItem from "@/components/posts/PostItem.vue"
|
import PostItem from "@/components/posts/PostItem.vue"
|
||||||
|
|
||||||
const props = defineProps<{ posts: any[]; loader: (opts: any) => Promise<any> }>()
|
const props = defineProps<{ variant?: any, posts: any[]; loader: (opts: any) => Promise<any> }>()
|
||||||
const emits = defineEmits(["update:posts"])
|
const emits = defineEmits(["update:posts"])
|
||||||
|
|
||||||
function updateItem(idx: number, data: any) {
|
function updateItem(idx: number, data: any) {
|
||||||
|
@ -26,12 +26,6 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
|
|
||||||
<v-snackbar v-model="status.added" :timeout="3000">Your react has been added into post.</v-snackbar>
|
|
||||||
<v-snackbar v-model="status.removed" :timeout="3000">Your react has been removed from post.</v-snackbar>
|
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -39,8 +33,10 @@
|
|||||||
import { request } from "@/scripts/request"
|
import { request } from "@/scripts/request"
|
||||||
import { getAtk, useUserinfo } from "@/stores/userinfo"
|
import { getAtk, useUserinfo } from "@/stores/userinfo"
|
||||||
import { reactive, ref } from "vue"
|
import { reactive, ref } from "vue"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
const id = useUserinfo()
|
const id = useUserinfo()
|
||||||
|
const {showSnackbar, showErrorSnackbar} = useUI()
|
||||||
|
|
||||||
const emits = defineEmits(["update"])
|
const emits = defineEmits(["update"])
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -62,9 +58,6 @@ function pickColor(): string {
|
|||||||
return colors[randomIndex]
|
return colors[randomIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = reactive({ added: false, removed: false })
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
|
|
||||||
async function reactPost(symbol: string, attitude: number) {
|
async function reactPost(symbol: string, attitude: number) {
|
||||||
const res = await request("interactive", `/api/p/${props.model}/${props.item?.id}/react`, {
|
const res = await request("interactive", `/api/p/${props.model}/${props.item?.id}/react`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -72,13 +65,13 @@ async function reactPost(symbol: string, attitude: number) {
|
|||||||
body: JSON.stringify({ symbol, attitude })
|
body: JSON.stringify({ symbol, attitude })
|
||||||
})
|
})
|
||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
status.added = true
|
showSnackbar("Your react has been added onto the post.")
|
||||||
emits("update", symbol, 1)
|
emits("update", symbol, 1)
|
||||||
} else if (res.status === 204) {
|
} else if (res.status === 204) {
|
||||||
status.removed = true
|
showSnackbar("Your react has been removed from the post.")
|
||||||
emits("update", symbol, -1)
|
emits("update", symbol, -1)
|
||||||
} else {
|
} else {
|
||||||
error.value = await res.text()
|
showErrorSnackbar(await res.text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -109,14 +109,10 @@
|
|||||||
<media ref="media" v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" />
|
<media ref="media" v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" />
|
||||||
<publish-area v-model:show="dialogs.area" v-model:value="data.realm_id" />
|
<publish-area v-model:show="dialogs.area" v-model:value="data.realm_id" />
|
||||||
|
|
||||||
<v-snackbar v-model="success" :timeout="3000">Your article has been published.</v-snackbar>
|
|
||||||
<v-snackbar v-model="uploading" :timeout="-1">
|
<v-snackbar v-model="uploading" :timeout="-1">
|
||||||
Uploading your media, please stand by...
|
Uploading your media, please stand by...
|
||||||
<v-progress-linear class="snackbar-progress" indeterminate />
|
<v-progress-linear class="snackbar-progress" indeterminate />
|
||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -127,8 +123,9 @@ import { useRealms } from "@/stores/realms"
|
|||||||
import { computed, reactive, ref, watch } from "vue"
|
import { computed, reactive, ref, watch } from "vue"
|
||||||
import { useRoute, useRouter } from "vue-router"
|
import { useRoute, useRouter } from "vue-router"
|
||||||
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
|
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
|
||||||
import Media from "@/components/publish/parts/Media.vue"
|
import Media from "@/components/publish/parts/PublishMedia.vue"
|
||||||
import PublishArea from "@/components/publish/parts/PublishArea.vue"
|
import PublishArea from "@/components/publish/parts/PublishArea.vue"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const realms = useRealms()
|
const realms = useRealms()
|
||||||
@ -160,10 +157,9 @@ const currentRealm = computed(() => {
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
const { showSnackbar, showErrorSnackbar } = useUI()
|
||||||
const success = ref(false)
|
|
||||||
const reverting = ref(false)
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const reverting = ref(false)
|
||||||
const uploading = ref(false)
|
const uploading = ref(false)
|
||||||
|
|
||||||
async function postArticle(evt: SubmitEvent) {
|
async function postArticle(evt: SubmitEvent) {
|
||||||
@ -187,15 +183,15 @@ async function postArticle(evt: SubmitEvent) {
|
|||||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAtk()}` },
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAtk()}` },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
if (res.status === 200) {
|
if (res.status !== 200) {
|
||||||
|
showErrorSnackbar(await res.text())
|
||||||
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
success.value = true
|
showSnackbar("Your article has been published.")
|
||||||
editor.show.article = false
|
editor.show.article = false
|
||||||
|
|
||||||
resetEditor(form)
|
resetEditor(form)
|
||||||
router.push({ name: "posts.details.articles", params: { alias: data.alias } })
|
await router.push({ name: "posts.details.articles", params: { alias: data.alias } })
|
||||||
} else {
|
|
||||||
error.value = await res.text()
|
|
||||||
}
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -262,10 +258,7 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.snackbar-progress {
|
.snackbar-progress {
|
||||||
margin-left: -16px;
|
margin: 12px -16px -14px;
|
||||||
margin-right: -16px;
|
|
||||||
margin-bottom: -14px;
|
|
||||||
margin-top: 12px;
|
|
||||||
width: calc(100% + 64px);
|
width: calc(100% + 64px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -19,11 +19,6 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-snackbar v-model="success" :timeout="3000">Your comment has been published.</v-snackbar>
|
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -31,7 +26,9 @@ import { request } from "@/scripts/request"
|
|||||||
import { useEditor } from "@/stores/editor"
|
import { useEditor } from "@/stores/editor"
|
||||||
import { getAtk } from "@/stores/userinfo"
|
import { getAtk } from "@/stores/userinfo"
|
||||||
import { computed, ref, watch } from "vue"
|
import { computed, ref, watch } from "vue"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
|
const { showSnackbar, showErrorSnackbar } = useUI()
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
const target = computed<any>(() => editor.related.comment_to)
|
const target = computed<any>(() => editor.related.comment_to)
|
||||||
@ -43,8 +40,6 @@ const postIdentifier = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
const success = ref(false)
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const data = ref<any>({
|
const data = ref<any>({
|
||||||
@ -70,10 +65,10 @@ async function postComment(evt: SubmitEvent) {
|
|||||||
})
|
})
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
form.reset()
|
form.reset()
|
||||||
success.value = true
|
showSnackbar("Your comment has been published.")
|
||||||
editor.show.comment = false
|
editor.show.comment = false
|
||||||
} else {
|
} else {
|
||||||
error.value = await res.text()
|
showErrorSnackbar(await res.text())
|
||||||
}
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
editor.done = true
|
editor.done = true
|
||||||
|
@ -75,14 +75,10 @@
|
|||||||
<media ref="media" v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" />
|
<media ref="media" v-model:show="dialogs.media" v-model:uploading="uploading" v-model:value="data.attachments" />
|
||||||
<publish-area v-model:show="dialogs.area" v-model:value="data.realm_id" />
|
<publish-area v-model:show="dialogs.area" v-model:value="data.realm_id" />
|
||||||
|
|
||||||
<v-snackbar v-model="success" :timeout="3000">Your post has been published.</v-snackbar>
|
|
||||||
<v-snackbar v-model="uploading" :timeout="-1">
|
<v-snackbar v-model="uploading" :timeout="-1">
|
||||||
Uploading your media, please stand by...
|
Uploading your media, please stand by...
|
||||||
<v-progress-linear class="snackbar-progress" indeterminate />
|
<v-progress-linear class="snackbar-progress" indeterminate />
|
||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -93,7 +89,8 @@ import { reactive, ref, watch } from "vue"
|
|||||||
import { useRoute, useRouter } from "vue-router"
|
import { useRoute, useRouter } from "vue-router"
|
||||||
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
|
import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue"
|
||||||
import PublishArea from "@/components/publish/parts/PublishArea.vue"
|
import PublishArea from "@/components/publish/parts/PublishArea.vue"
|
||||||
import Media from "@/components/publish/parts/Media.vue"
|
import Media from "@/components/publish/parts/PublishMedia.vue"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
@ -111,8 +108,7 @@ const data = ref<any>({
|
|||||||
attachments: []
|
attachments: []
|
||||||
})
|
})
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
const { showSnackbar, showErrorSnackbar } = useUI()
|
||||||
const success = ref(false)
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const uploading = ref(false)
|
const uploading = ref(false)
|
||||||
|
|
||||||
@ -135,14 +131,15 @@ async function postMoment(evt: SubmitEvent) {
|
|||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
|
resetEditor(form)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
success.value = true
|
|
||||||
editor.show.moment = false
|
editor.show.moment = false
|
||||||
|
|
||||||
resetEditor(form)
|
showSnackbar("Your post has been published.")
|
||||||
router.push({ name: "posts.details.moments", params: { alias: data.alias } })
|
|
||||||
|
await router.push({ name: "posts.details.moments", params: { alias: data.alias } })
|
||||||
} else {
|
} else {
|
||||||
error.value = await res.text()
|
showErrorSnackbar(await res.text())
|
||||||
}
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -188,10 +185,7 @@ watch(
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.snackbar-progress {
|
.snackbar-progress {
|
||||||
margin-left: -16px;
|
margin: 12px -16px -14px;
|
||||||
margin-right: -16px;
|
|
||||||
margin-bottom: -14px;
|
|
||||||
margin-top: 12px;
|
|
||||||
width: calc(100% + 64px);
|
width: calc(100% + 64px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -12,11 +12,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-snackbar v-model="success" :timeout="3000">The post has been deleted.</v-snackbar>
|
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -24,11 +19,11 @@ import { request } from "@/scripts/request"
|
|||||||
import { useEditor } from "@/stores/editor"
|
import { useEditor } from "@/stores/editor"
|
||||||
import { getAtk } from "@/stores/userinfo"
|
import { getAtk } from "@/stores/userinfo"
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
const {showSnackbar, showErrorSnackbar} = useUI()
|
||||||
const success = ref(false)
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
async function deletePost() {
|
async function deletePost() {
|
||||||
@ -41,9 +36,9 @@ async function deletePost() {
|
|||||||
headers: { Authorization: `Bearer ${await getAtk()}` }
|
headers: { Authorization: `Bearer ${await getAtk()}` }
|
||||||
})
|
})
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
error.value = await res.text()
|
showErrorSnackbar(await res.text())
|
||||||
} else {
|
} else {
|
||||||
success.value = true
|
showSnackbar("The post has been deleted.")
|
||||||
editor.show.delete = false
|
editor.show.delete = false
|
||||||
editor.related.delete_to = null
|
editor.related.delete_to = null
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-dialog
|
<v-dialog
|
||||||
eager
|
|
||||||
class="max-w-[540px]"
|
class="max-w-[540px]"
|
||||||
:model-value="props.show"
|
:model-value="props.show"
|
||||||
@update:model-value="(val) => emits('update:show', val)"
|
@update:model-value="(val) => emits('update:show', val)"
|
@ -22,7 +22,7 @@ const realms = useRealms()
|
|||||||
|
|
||||||
const props = defineProps<{ item: any }>()
|
const props = defineProps<{ item: any }>()
|
||||||
|
|
||||||
const isOwned = computed(() => props.item?.account_id === id.userinfo.data.id)
|
const isOwned = computed(() => props.item?.account_id === id.userinfo.idSet?.interactive)
|
||||||
|
|
||||||
function editRealm() {
|
function editRealm() {
|
||||||
realms.related.edit_to = props.item
|
realms.related.edit_to = props.item
|
||||||
|
@ -3,20 +3,15 @@
|
|||||||
<template #text>
|
<template #text>
|
||||||
You are deleting a realm
|
You are deleting a realm
|
||||||
<b>{{ realms.related.delete_to?.name }}</b> <br />
|
<b>{{ realms.related.delete_to?.name }}</b> <br />
|
||||||
All posts belonging to this domain will be deleted and never appear again. Are you confirm?
|
All posts belonging to this realm will be deleted and never appear again. Are you confirm?
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="w-full flex justify-end">
|
<div class="w-full flex justify-end">
|
||||||
<v-btn color="grey-darken-3" @click="realms.show.delete = false">Not really</v-btn>
|
<v-btn color="grey-darken-3" @click="realms.show.delete = false">Not really</v-btn>
|
||||||
<v-btn color="error" :disabled="loading" @click="deletePost">Yes</v-btn>
|
<v-btn color="error" :disabled="loading" @click="deleteRealm">Yes</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-snackbar v-model="success" :timeout="3000">The realm has been deleted.</v-snackbar>
|
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -25,18 +20,18 @@ import { useRealms } from "@/stores/realms"
|
|||||||
import { getAtk } from "@/stores/userinfo"
|
import { getAtk } from "@/stores/userinfo"
|
||||||
import { useRoute, useRouter } from "vue-router"
|
import { useRoute, useRouter } from "vue-router"
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
|
const { showSnackbar, showErrorSnackbar } = useUI()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const realms = useRealms()
|
const realms = useRealms()
|
||||||
|
|
||||||
const emits = defineEmits(["relist"])
|
const emits = defineEmits(["relist"])
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
const success = ref(false)
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
async function deletePost() {
|
async function deleteRealm() {
|
||||||
const target = realms.related.delete_to
|
const target = realms.related.delete_to
|
||||||
const url = `/api/realms/${target.id}`
|
const url = `/api/realms/${target.id}`
|
||||||
|
|
||||||
@ -46,14 +41,14 @@ async function deletePost() {
|
|||||||
headers: { Authorization: `Bearer ${await getAtk()}` }
|
headers: { Authorization: `Bearer ${await getAtk()}` }
|
||||||
})
|
})
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
error.value = await res.text()
|
showErrorSnackbar(await res.text())
|
||||||
} else {
|
} else {
|
||||||
success.value = true
|
showSnackbar("The realm has been deleted.")
|
||||||
realms.show.delete = false
|
realms.show.delete = false
|
||||||
realms.related.delete_to = null
|
realms.related.delete_to = null
|
||||||
emits("relist")
|
emits("relist")
|
||||||
if (route.name?.toString()?.startsWith("realm")) {
|
if (route.name?.toString()?.includes("realm")) {
|
||||||
router.push({ name: "explore" })
|
await router.push({ name: "explore" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
@ -22,9 +22,6 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -32,6 +29,7 @@ import { ref, watch } from "vue"
|
|||||||
import { getAtk } from "@/stores/userinfo"
|
import { getAtk } from "@/stores/userinfo"
|
||||||
import { useRealms } from "@/stores/realms"
|
import { useRealms } from "@/stores/realms"
|
||||||
import { request } from "@/scripts/request"
|
import { request } from "@/scripts/request"
|
||||||
|
import { useUI } from "@/stores/ui"
|
||||||
|
|
||||||
const emits = defineEmits(["relist"])
|
const emits = defineEmits(["relist"])
|
||||||
|
|
||||||
@ -43,7 +41,7 @@ const realmTypeOptions = [
|
|||||||
{ label: "Private Realm", value: 2 }
|
{ label: "Private Realm", value: 2 }
|
||||||
]
|
]
|
||||||
|
|
||||||
const error = ref<null | string>(null)
|
const { showErrorSnackbar } = useUI()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const data = ref({
|
const data = ref({
|
||||||
@ -67,7 +65,7 @@ async function submit(evt: SubmitEvent) {
|
|||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
error.value = await res.text()
|
showErrorSnackbar(await res.text())
|
||||||
} else {
|
} else {
|
||||||
emits("relist")
|
emits("relist")
|
||||||
form.reset()
|
form.reset()
|
||||||
|