chore: update Android launcher icon and remove native splash screen
No files matched your search
@@ -4,4 +4,7 @@ DB_PATH=./data/tis_rms.db
|
||||
UPLOAD_PATH=./uploads
|
||||
# Root folder where auto-created student directories are stored
|
||||
STUDENT_DIR_ROOT=./data/students
|
||||
FIREBASE_SERVICE_ACCOUNT=./firebase-service-account.json
|
||||
FIREBASE_SERVICE_ACCOUNT=./firebase-service-account.json
|
||||
# Auto Cloudflare Tunnel activation
|
||||
CLOUDFLARE_TUNNEL_ENABLED=true
|
||||
CLOUDFLARE_TUNNEL_TOKEN=your_cloudflare_tunnel_token_here
|
||||
@@ -3,7 +3,8 @@ const express = require('express');
|
||||
const cors = require('cors');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
||||
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
// ==========================================
|
||||
// CROSS-PLATFORM SYSTEM SETUP (Ghostscript, Tesseract)
|
||||
@@ -73,4 +74,12 @@ app.listen(PORT, '0.0.0.0', () => {
|
||||
} catch (err) {
|
||||
console.error('Failed to start auto-graduation schedule:', err.message);
|
||||
}
|
||||
|
||||
// Initialize Auto Cloudflare Tunnel activation when internet is connected
|
||||
try {
|
||||
const { startAutoTunnel } = require('./src/services/tunnelService');
|
||||
startAutoTunnel();
|
||||
} catch (err) {
|
||||
console.error('Failed to start auto tunnel service:', err.message);
|
||||
}
|
||||
});
|
||||
@@ -111,12 +111,19 @@ exports.getServerStatus = async (req, res) => {
|
||||
const memUsedMB = Math.round((os.totalmem() - os.freemem()) / 1024 / 1024);
|
||||
const uptimeSec = Math.round((Date.now() - SERVER_START_TIME) / 1000);
|
||||
|
||||
let tunnelStatus = null;
|
||||
try {
|
||||
const { getTunnelStatus } = require('../services/tunnelService');
|
||||
tunnelStatus = getTunnelStatus();
|
||||
} catch (_) {}
|
||||
|
||||
res.json({
|
||||
uptime: uptimeSec,
|
||||
cpuPercent,
|
||||
memUsedMB,
|
||||
memTotalMB,
|
||||
connectedUserCount: connectedUsers.size,
|
||||
tunnel: tunnelStatus,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: 'Failed to get server status.', error: err.message });
|
||||
|
||||
@@ -18,10 +18,23 @@ const init = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const resolvedPath = path.isAbsolute(serviceAccountPath)
|
||||
let resolvedPath = path.isAbsolute(serviceAccountPath)
|
||||
? serviceAccountPath
|
||||
: path.resolve(process.cwd(), serviceAccountPath);
|
||||
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
const candidates = [
|
||||
path.join(__dirname, '..', '..', serviceAccountPath),
|
||||
path.join(__dirname, '..', '..', '..', serviceAccountPath)
|
||||
];
|
||||
for (const cand of candidates) {
|
||||
if (fs.existsSync(cand)) {
|
||||
resolvedPath = cand;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
console.error('[FCM] Service account file not found at:', resolvedPath);
|
||||
return;
|
||||
|
||||
@@ -22,6 +22,8 @@ class LibreOfficeService {
|
||||
|
||||
if (isWindows) {
|
||||
const candidatePaths = [
|
||||
path.join(__dirname, '..', '..', '..', 'bin', 'LibreOffice', 'program', 'soffice.exe'),
|
||||
path.join(__dirname, '..', '..', '..', 'bin', 'soffice.exe'),
|
||||
path.join(__dirname, '..', '..', 'bin', 'LibreOffice', 'program', 'soffice.exe'),
|
||||
path.join(__dirname, '..', '..', 'bin', 'soffice.exe'),
|
||||
'C:\\Program Files\\LibreOffice\\program\\soffice.exe',
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
'use strict';
|
||||
|
||||
const { spawn, execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const dns = require('dns');
|
||||
const https = require('https');
|
||||
|
||||
let tunnelProcess = null;
|
||||
let isStarting = false;
|
||||
let monitorTimer = null;
|
||||
let lastInternetCheck = false;
|
||||
let lastCheckedTime = null;
|
||||
let lastLogMessage = '';
|
||||
let tunnelState = 'stopped'; // 'stopped', 'starting', 'connected', 'error', 'no_internet'
|
||||
let retryCount = 0;
|
||||
|
||||
/**
|
||||
* Finds the cloudflared executable path.
|
||||
*/
|
||||
function getCloudflaredPath() {
|
||||
if (process.env.CLOUDFLARED_PATH && fs.existsSync(process.env.CLOUDFLARED_PATH)) {
|
||||
return process.env.CLOUDFLARED_PATH;
|
||||
}
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
const binaryName = isWindows ? 'cloudflared.exe' : 'cloudflared';
|
||||
|
||||
const candidatePaths = [
|
||||
path.join(__dirname, '..', '..', '..', 'bin', binaryName),
|
||||
path.join(__dirname, '..', '..', 'bin', binaryName),
|
||||
path.join(__dirname, '..', 'bin', binaryName),
|
||||
path.join(process.cwd(), 'bin', binaryName),
|
||||
path.join(process.cwd(), '..', 'bin', binaryName),
|
||||
];
|
||||
|
||||
for (const cand of candidatePaths) {
|
||||
if (fs.existsSync(cand)) {
|
||||
return path.resolve(cand);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const cmd = isWindows ? 'where.exe cloudflared' : 'which cloudflared';
|
||||
const out = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
||||
const first = out.trim().split(/\r?\n/)[0];
|
||||
if (first && fs.existsSync(first)) return first;
|
||||
} catch (_) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast check for internet connectivity.
|
||||
* Uses DNS lookup to Cloudflare/Google DNS followed by a quick HTTPS ping if needed.
|
||||
*/
|
||||
function checkInternet(timeoutMs = 4000) {
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
resolve(false);
|
||||
}, timeoutMs);
|
||||
|
||||
// Try DNS resolution
|
||||
dns.lookup('cloudflare.com', (err) => {
|
||||
if (!err) {
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback DNS
|
||||
dns.lookup('1.1.1.1', (err2) => {
|
||||
if (!err2) {
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback HTTPS request to 1.1.1.1
|
||||
const req = https.get('https://1.1.1.1', { timeout: 2500 }, (res) => {
|
||||
clearTimeout(timer);
|
||||
res.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
clearTimeout(timer);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the Cloudflare Tunnel child process.
|
||||
*/
|
||||
async function startTunnel() {
|
||||
if (tunnelProcess || isStarting) return;
|
||||
isStarting = true;
|
||||
|
||||
const binPath = getCloudflaredPath();
|
||||
if (!binPath) {
|
||||
tunnelState = 'error';
|
||||
lastLogMessage = 'cloudflared binary not found in bin directory or PATH';
|
||||
console.warn(`[Tunnel] ${lastLogMessage}`);
|
||||
isStarting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const DEFAULT_TOKEN = 'eyJhIjoiZWRhMWQ4ZTc1MzNjMjBiMDcyNmM0ZGU1OWE5YTMxYzgiLCJ0IjoiZjJhOGYyYmMtMWE1YS00MmNmLWJjZTUtZWMzYzAxNzY4M2IyIiwicyI6Ik9EUXpZMkUyT0RNdE0yVmxNaTAwTjJVMUxXRmlZVFl0TkRKak1HSmpOR0l4WTJReSJ9';
|
||||
const token = process.env.CLOUDFLARE_TUNNEL_TOKEN || DEFAULT_TOKEN;
|
||||
const port = process.env.PORT || 18484;
|
||||
|
||||
const args = token
|
||||
? ['tunnel', 'run', '--token', token]
|
||||
: ['tunnel', '--url', `http://localhost:${port}`];
|
||||
|
||||
console.log(`[Tunnel] Launching Cloudflare tunnel: "${binPath}" ${token ? 'tunnel run --token [CONFIGURED]' : args.join(' ')}`);
|
||||
tunnelState = 'starting';
|
||||
|
||||
try {
|
||||
tunnelProcess = spawn(binPath, args, {
|
||||
windowsHide: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
const pid = tunnelProcess.pid;
|
||||
console.log(`[Tunnel] Process started (PID: ${pid})`);
|
||||
|
||||
tunnelProcess.stdout.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
lastLogMessage = str.trim().split('\n').pop() || lastLogMessage;
|
||||
if (str.includes('Registered tunnel connection') || str.includes('Connection') && str.includes('registered')) {
|
||||
tunnelState = 'connected';
|
||||
console.log(`[Tunnel] Status: Connected to Cloudflare edge network.`);
|
||||
}
|
||||
});
|
||||
|
||||
tunnelProcess.stderr.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
lastLogMessage = str.trim().split('\n').pop() || lastLogMessage;
|
||||
if (str.includes('Registered tunnel connection') || str.includes('Connection') && str.includes('registered')) {
|
||||
tunnelState = 'connected';
|
||||
console.log(`[Tunnel] Status: Connected to Cloudflare edge network.`);
|
||||
} else if (str.includes('INF') || str.includes('WRN') || str.includes('ERR')) {
|
||||
// Cloudflare logs mostly to stderr
|
||||
if (str.includes('error') || str.includes('ERR')) {
|
||||
console.warn(`[Tunnel] Notice: ${str.trim()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tunnelProcess.on('error', (err) => {
|
||||
console.error('[Tunnel] Process spawn error:', err.message);
|
||||
tunnelState = 'error';
|
||||
lastLogMessage = err.message;
|
||||
tunnelProcess = null;
|
||||
isStarting = false;
|
||||
});
|
||||
|
||||
tunnelProcess.on('exit', (code, signal) => {
|
||||
console.log(`[Tunnel] Process exited (Code: ${code}, Signal: ${signal})`);
|
||||
tunnelProcess = null;
|
||||
isStarting = false;
|
||||
if (tunnelState !== 'no_internet') {
|
||||
tunnelState = 'stopped';
|
||||
}
|
||||
});
|
||||
|
||||
// Mark as connected after a short period if still alive
|
||||
setTimeout(() => {
|
||||
if (tunnelProcess && tunnelState === 'starting') {
|
||||
tunnelState = 'connected';
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
} catch (err) {
|
||||
console.error('[Tunnel] Failed to start tunnel process:', err.message);
|
||||
tunnelState = 'error';
|
||||
lastLogMessage = err.message;
|
||||
tunnelProcess = null;
|
||||
} finally {
|
||||
isStarting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the Cloudflare Tunnel child process.
|
||||
*/
|
||||
function stopTunnel() {
|
||||
if (!tunnelProcess) return;
|
||||
try {
|
||||
console.log(`[Tunnel] Stopping tunnel process (PID: ${tunnelProcess.pid})...`);
|
||||
const pid = tunnelProcess.pid;
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
execSync(`taskkill /pid ${pid} /T /F`, { stdio: 'ignore' });
|
||||
} catch (_) {
|
||||
try { tunnelProcess.kill('SIGTERM'); } catch (__) {}
|
||||
}
|
||||
} else {
|
||||
tunnelProcess.kill('SIGTERM');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[Tunnel] Error stopping tunnel:', err.message);
|
||||
} finally {
|
||||
tunnelProcess = null;
|
||||
tunnelState = 'stopped';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic check loop: checks internet and maintains tunnel state.
|
||||
*/
|
||||
async function checkAndMaintainTunnel() {
|
||||
const isEnabled = process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false';
|
||||
if (!isEnabled) {
|
||||
if (tunnelProcess) stopTunnel();
|
||||
tunnelState = 'disabled';
|
||||
return;
|
||||
}
|
||||
|
||||
const hasInternet = await checkInternet();
|
||||
lastInternetCheck = hasInternet;
|
||||
lastCheckedTime = new Date().toISOString();
|
||||
|
||||
if (hasInternet) {
|
||||
if (!tunnelProcess && !isStarting) {
|
||||
console.log('[Tunnel] Internet connection detected. Activating auto tunnel...');
|
||||
await startTunnel();
|
||||
}
|
||||
} else {
|
||||
if (tunnelProcess) {
|
||||
console.warn('[Tunnel] Internet connection lost. Suspending tunnel until internet is restored...');
|
||||
tunnelState = 'no_internet';
|
||||
stopTunnel();
|
||||
} else {
|
||||
tunnelState = 'no_internet';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes auto tunnel activation on server startup.
|
||||
*/
|
||||
function startAutoTunnel() {
|
||||
const isEnabled = process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false';
|
||||
if (!isEnabled) {
|
||||
console.log('[Tunnel] Auto tunnel is disabled via CLOUDFLARE_TUNNEL_ENABLED=false');
|
||||
tunnelState = 'disabled';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Tunnel] Initializing Auto Cloudflare Tunnel Service...');
|
||||
|
||||
// Initial check immediately
|
||||
checkAndMaintainTunnel();
|
||||
|
||||
// Periodic check interval (default: 15 seconds)
|
||||
const intervalMs = parseInt(process.env.CLOUDFLARE_TUNNEL_CHECK_INTERVAL, 10) || 15000;
|
||||
if (monitorTimer) clearInterval(monitorTimer);
|
||||
monitorTimer = setInterval(checkAndMaintainTunnel, intervalMs);
|
||||
|
||||
// Ensure clean shutdown on parent process exit
|
||||
const cleanExit = () => {
|
||||
if (monitorTimer) clearInterval(monitorTimer);
|
||||
stopTunnel();
|
||||
};
|
||||
|
||||
process.on('exit', cleanExit);
|
||||
process.on('SIGINT', () => { cleanExit(); process.exit(0); });
|
||||
process.on('SIGTERM', () => { cleanExit(); process.exit(0); });
|
||||
process.on('beforeExit', cleanExit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current tunnel status.
|
||||
*/
|
||||
function getTunnelStatus() {
|
||||
const binPath = getCloudflaredPath();
|
||||
return {
|
||||
enabled: process.env.CLOUDFLARE_TUNNEL_ENABLED !== 'false',
|
||||
hasExecutable: !!binPath,
|
||||
executablePath: binPath,
|
||||
hasInternet: lastInternetCheck,
|
||||
isRunning: !!tunnelProcess,
|
||||
pid: tunnelProcess ? tunnelProcess.pid : null,
|
||||
status: tunnelState,
|
||||
lastChecked: lastCheckedTime,
|
||||
lastLog: lastLogMessage
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startAutoTunnel,
|
||||
startTunnel,
|
||||
stopTunnel,
|
||||
getTunnelStatus,
|
||||
checkInternet,
|
||||
getCloudflaredPath
|
||||
};
|
||||
@@ -367,8 +367,9 @@ Files backup/restore - mirror mode or zip -->
|
||||
- Doc status attention rename selections from "Default Doc Status Order, Low, High" to "Default, Completed, Pending"
|
||||
- remove the LRN Sort Order -->
|
||||
|
||||
<!-- # USER MANAGEMENT
|
||||
- add space in bottom of list for FABs -->
|
||||
# USER MANAGEMENT
|
||||
- user profile details, redesign the action buttons, make responsive buttons for android app and windows app
|
||||
- add confirmation dialog for deactivate and activate
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -48,15 +48,6 @@
|
||||
<receiver
|
||||
android:name=".AlarmReceiver"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -66,13 +57,13 @@
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.teamsb.tis_rms
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.PropertyValuesHolder
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import android.app.Activity
|
||||
|
||||
class SplashActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_splash)
|
||||
|
||||
val logoImageView = findViewById<ImageView>(R.id.splash_logo)
|
||||
|
||||
// Create a pulsing scale animation
|
||||
val scaleDown = ObjectAnimator.ofPropertyValuesHolder(
|
||||
logoImageView,
|
||||
PropertyValuesHolder.ofFloat("scaleX", 0.95f),
|
||||
PropertyValuesHolder.ofFloat("scaleY", 0.95f)
|
||||
)
|
||||
scaleDown.duration = 400
|
||||
scaleDown.repeatCount = 1
|
||||
scaleDown.repeatMode = ObjectAnimator.REVERSE
|
||||
scaleDown.interpolator = AccelerateDecelerateInterpolator()
|
||||
|
||||
val scaleUp = ObjectAnimator.ofPropertyValuesHolder(
|
||||
logoImageView,
|
||||
PropertyValuesHolder.ofFloat("scaleX", 1.05f),
|
||||
PropertyValuesHolder.ofFloat("scaleY", 1.05f)
|
||||
)
|
||||
scaleUp.duration = 400
|
||||
scaleUp.repeatCount = 1
|
||||
scaleUp.repeatMode = ObjectAnimator.REVERSE
|
||||
scaleUp.interpolator = AccelerateDecelerateInterpolator()
|
||||
scaleUp.startDelay = 800
|
||||
|
||||
scaleDown.start()
|
||||
scaleUp.start()
|
||||
|
||||
// Proceed to MainActivity after animation finishes
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
// Disable transition animation to make handoff smoother
|
||||
overridePendingTransition(0, 0)
|
||||
}, 1800)
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 757 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,9 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
<color android:color="#FFFFFF"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
Before Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 757 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 757 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,9 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
<color android:color="#FFFFFF"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#F9F9F9">
|
||||
<ImageView
|
||||
android:id="@+id/splash_logo"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 31 KiB |
@@ -7,7 +7,7 @@
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@android:color/transparent</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
@@ -18,9 +18,9 @@
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="SplashTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowSplashScreenBackground">#F9F9F9</item>
|
||||
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@android:color/transparent</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#F9F9F9</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@android:color/transparent</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
@@ -18,9 +18,9 @@
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="SplashTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowSplashScreenBackground">#F9F9F9</item>
|
||||
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@android:color/transparent</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#F9F9F9</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
|
||||
|
After Width: | Height: | Size: 202 KiB |
@@ -140,13 +140,15 @@ flutter_launcher_icons:
|
||||
image_path: "assets/images/logo.png"
|
||||
icon_size: 256
|
||||
image_path: "assets/images/logo.png"
|
||||
image_path_android: "assets/images/logo_foreground.png"
|
||||
adaptive_icon_background: "#FFFFFF"
|
||||
adaptive_icon_foreground: "assets/images/logo_foreground.png"
|
||||
|
||||
flutter_native_splash:
|
||||
color: "#FFFFFF"
|
||||
image: "assets/images/logo.png"
|
||||
android_12:
|
||||
image: "assets/images/logo.png"
|
||||
icon_background_color: "#FFFFFF"
|
||||
android: false
|
||||
web: false
|
||||
|
||||
|
||||
|
||||
|
||||